File "mydata_aade.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/admin/helpers/einvoicing/drivers/mydata_aade.php
File size: 147.11 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* @package VikBooking
* @subpackage com_vikbooking
* @author Alessio Gaggii - E4J srl
* @copyright Copyright (C) 2024 E4J srl. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
* @link https://vikwp.com
*/
defined('ABSPATH') or die('No script kiddies please!');
/**
* MydataAade child Class of VikBookingEInvoicing
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
class VikBookingEInvoicingMydataAade extends VikBookingEInvoicing
{
/**
* Property 'defaultKeySort' is used by the View that renders the driver.
*
* @var string
*/
public $defaultKeySort = 'ts';
/**
* Property 'defaultKeyOrder' is used by the View that renders the driver.
*
* @var string
*/
public $defaultKeyOrder = 'ASC';
/**
* The path to this driver helper directory. Used only by this driver.
*
* @var string
*/
protected $driverHelperPath = '';
/**
* An array of session filters.
*
* @var array
*/
protected $sessionFilters;
/**
* An array of bookings.
*
* @var array
*/
protected $bookings;
/**
* @var array
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
protected $environmental_fee_details = [];
/**
* Class constructor should define the name of the driver and
* other vars. Call the parent constructor to define the DB object.
*/
public function __construct()
{
$this->driverFile = basename(__FILE__, '.php');
$this->driverName = "myDATA - ΑΑΔΕ Greece";
$this->driverFilters = [];
$this->driverButtons = [];
// driver helper dir path
$this->driverHelperPath = dirname(__FILE__) . DIRECTORY_SEPARATOR . str_replace(' ', '', ucwords(str_replace('_', ' ', $this->driverFile))) . DIRECTORY_SEPARATOR;
// this driver has settings
$this->hasSettings = true;
// reset session filters
$this->sessionFilters = [];
// reset bookings array
$this->bookings = [];
$this->cols = [];
$this->rows = [];
$this->footerRow = [];
// require class constants
$this->importHelper($this->driverHelperPath . 'constants.php');
parent::__construct();
}
/**
* Returns the name of this file without .php.
*
* @return string
*/
public function getFileName()
{
return $this->driverFile;
}
/**
* Returns the name of this driver.
*
* @return string
*/
public function getName()
{
return $this->driverName;
}
/**
* Returns the filters of this driver.
*
* @return array
*/
public function getFilters()
{
if (count($this->driverFilters)) {
// do not run this method twice, as it could load JS and CSS files.
return $this->driverFilters;
}
// session filters
$sessfilters = $this->loadSessionFilters();
// get VBO Application Object
$vbo_app = VikBooking::getVboApplication();
// load the jQuery UI Datepicker
$this->loadDatePicker();
// date format
$df = $this->getDateFormat();
// request variables
$pfromdate = VikRequest::getString('fromdate', '', 'request');
$ptodate = VikRequest::getString('todate', '', 'request');
$peinvtype = VikRequest::getInt('einvtype', 0, 'request');
$peinvkword = VikRequest::getString('einvkword', '', 'request');
$pdatetype = VikRequest::getString('datetype', $this->getSessionFilter('datetype', ''), 'request');
// js lang vars
JText::script('VBDELCONFIRM');
// From Date Filter
$filter_opt = array(
'label' => '<label for="fromdate">'.JText::translate('VBOREPORTSDATEFROM').'</label>',
'html' => '<input type="text" id="fromdate" name="fromdate" value="" class="vbo-einvoicing-datepicker vbo-einvoicing-datepicker-from" size="12" autocomplete="off" />',
'type' => 'calendar',
'name' => 'fromdate'
);
array_push($this->driverFilters, $filter_opt);
// To Date Filter
$filter_opt = array(
'label' => '<label for="todate">'.JText::translate('VBOREPORTSDATETO').'</label>',
'html' => '<input type="text" id="todate" name="todate" value="" class="vbo-einvoicing-datepicker vbo-einvoicing-datepicker-to" size="12" autocomplete="off" />',
'type' => 'calendar',
'name' => 'todate'
);
array_push($this->driverFilters, $filter_opt);
// jQuery code for the datepicker calendars and other events
if (empty($pfromdate) && empty($ptodate)) {
// if both request values are empty, take them from the session
$pfromdate = $this->getSessionFilter('fromdate');
$ptodate = $this->getSessionFilter('todate');
}
$js = '
jQuery(function() {
jQuery(".vbo-einvoicing-datepicker:input").datepicker({
maxDate: "+1y",
dateFormat: "'.$this->getDateFormat('jui').'",
onSelect: vboEInvoicingCheckDates
});
'.(!empty($pfromdate) && empty($peinvkword) ? 'jQuery(".vbo-einvoicing-datepicker-from").datepicker("setDate", "'.$pfromdate.'");' : '').'
'.(!empty($ptodate) && empty($peinvkword) ? 'jQuery(".vbo-einvoicing-datepicker-to").datepicker("setDate", "'.$ptodate.'");' : '').'
jQuery("#monyear").change(function() {
var monopt = jQuery(this).find("option:selected");
if (monopt && monopt.length && monopt.val().length) {
var from = monopt.attr("data-from");
var to = monopt.attr("data-to");
jQuery(".vbo-einvoicing-datepicker-from").datepicker("setDate", from);
jQuery(".vbo-einvoicing-datepicker-to").datepicker("setDate", to);
jQuery("#einvkword").val("");
}
});
jQuery(".vbo-einvoicing-selaction").change(function() {
var prop = "excludebid"+jQuery(this).attr("data-bid");
var pobj = {};
var actval = parseInt(jQuery(this).val());
pobj[prop] = actval;
vboSetFilters(pobj, false);
if (actval > 0) {
// update cell data attribute for CSS to not-generate
jQuery(this).closest("td").attr("data-einvaction", 0);
} else {
// update cell data attribute for CSS to generate
jQuery(this).closest("td").attr("data-einvaction", 1);
}
});
jQuery(".vbo-einvoicing-existaction").change(function() {
var prop = "regeneratebid"+jQuery(this).attr("data-bid");
var propexcl = "excludesendbid"+jQuery(this).attr("data-bid");
var einvid = parseInt(jQuery(this).val());
var pobj = {};
if (einvid > 0) {
// update cell data attribute for CSS to generate
jQuery(this).closest("td").attr("data-einvaction", 1);
// set re-generate and exclude send
pobj[prop] = einvid;
pobj[propexcl] = 1;
} else {
if (einvid < 0) {
// update cell data attribute for CSS to not-transmit
jQuery(this).closest("td").attr("data-einvaction", 0);
// set exclude send and not re-generate
pobj[prop] = 0;
pobj[propexcl] = 1;
} else {
// update cell data attribute for CSS to transmit (value = 0)
jQuery(this).closest("td").attr("data-einvaction", -2);
// set send and not re-generate
pobj[prop] = 0;
pobj[propexcl] = 0;
}
}
vboSetFilters(pobj, false);
});
jQuery(".vbo-einvoicing-sentaction").change(function() {
var propregen = "regeneratebid"+jQuery(this).attr("data-bid");
var propresend = "resendbid"+jQuery(this).attr("data-bid");
var curval = jQuery(this).val();
var splitval = curval.split("-");
var einvid = parseInt(splitval[0]);
var pobj = {};
if (einvid === 0) {
// update cell data attribute for CSS to transmitted
jQuery(this).closest("td").attr("data-einvaction", -1);
pobj[propregen] = einvid;
pobj[propresend] = einvid;
} else {
if (splitval[1] == "regen") {
// update cell data attribute for CSS to generate
jQuery(this).closest("td").attr("data-einvaction", 1);
pobj[propregen] = einvid;
pobj[propresend] = 0;
} else if (splitval[1] == "resend") {
// update cell data attribute for CSS to transmitted
jQuery(this).closest("td").attr("data-einvaction", -1);
pobj[propregen] = 0;
pobj[propresend] = einvid;
}
}
vboSetFilters(pobj, false);
});
jQuery(".vbo-driver-output-vieweinv").click(function() {
var id = jQuery(this).attr("data-einvid");
vboSetFilters({einvid: id}, false);
vboDriverDoAction("viewEInvoice", true);
});
jQuery(".vbo-driver-output-editeinv").click(function() {
var id = jQuery(this).attr("data-einvid");
var bid = jQuery(this).attr("data-envfeebid");
vboSetFilters({drivercontent: "editEInvoice", einvid: id, envfeebid: (bid || null)}, true);
});
jQuery(".vbo-driver-output-rmeinv").click(function() {
var id = jQuery(this).attr("data-einvid");
if (confirm(Joomla.JText._("VBDELCONFIRM"))) {
vboSetFilters({einvid: id}, false);
vboDriverDoAction("removeEInvoice", false);
}
});
});
function vboEInvoicingCheckDates(selectedDate, inst) {
if (selectedDate === null || inst === null) {
return;
}
jQuery("#monyear").val("");
jQuery("#einvkword").val("");
var cur_from_date = jQuery(this).val();
if (jQuery(this).hasClass("vbo-einvoicing-datepicker-from") && cur_from_date.length) {
var nowstart = jQuery(this).datepicker("getDate");
var nowstartdate = new Date(nowstart.getTime());
jQuery(".vbo-einvoicing-datepicker-to").datepicker("option", {minDate: nowstartdate});
}
}';
$this->setScript($js);
// month-year filter
$q = "SELECT MIN(`for_date`) AS `mindate`, MAX(`for_date`) AS `maxdate` FROM `#__vikbooking_einvoicing_data`;";
$this->dbo->setQuery($q);
$minmax = $this->dbo->loadAssoc();
if ($minmax) {
if (!empty($minmax['mindate']) && !empty($minmax['maxdate'])) {
$infomin = getdate(strtotime($minmax['mindate']));
$infomax = getdate(strtotime($minmax['maxdate']));
$startts = mktime(0, 0, 0, $infomin['mon'], 1, $infomin['year']);
$lastts = mktime(23, 59, 59, $infomax['mon'], date('t', $infomax[0]), $infomax['year']);
$monthys = [];
while ($startts < $lastts) {
array_push($monthys, array(
'mon' => $infomin['mon'],
'year' => $infomin['year'],
'from' => $startts,
'to' => mktime(0, 0, 0, $infomin['mon'], date('t', $infomin[0]), $infomin['year'])
));
$startts = mktime(0, 0, 0, ($infomin['mon'] + 1), 1, $infomin['year']);
$infomin = getdate($startts);
}
$opts = '';
foreach ($monthys as $my) {
$dfrom = date($df, $my['from']);
$dto = date($df, $my['to']);
$selectedstat = $pfromdate == $dfrom && $ptodate == $dto ? ' selected="selected"' : '';
$opts .= '<option value="'.$my['from'].'" data-from="'.$dfrom.'" data-to="'.$dto.'"'.$selectedstat.'>'.$this->getMonthString($my['mon']).' '.$my['year'].'</option>';
}
$filter_opt = array(
'label' => '<label for="monyear">' . JText::translate('VBPVIEWRESTRICTIONSTWO') . '</label>',
'html' => '<select name="monyear" id="monyear"><option value=""></option>'.$opts.'</select>',
'type' => 'select',
'name' => 'monyear'
);
array_push($this->driverFilters, $filter_opt);
}
}
// date type filter
$filter_opt = array(
'label' => '<label for="datetype">' . JText::translate('VBPVIEWORDERSONE') . '</label>',
'html' => '<select name="datetype" id="datetype">
<option value="ts"'.($pdatetype == 'ts' ? ' selected="selected"' : '').'>' . JText::translate('VBRENTALORD') . '</option>
<option value="checkin"'.($pdatetype == 'checkin' ? ' selected="selected"' : '').'>' . JText::translate('VBPICKUPAT') . '</option>
<option value="checkout"'.($pdatetype == 'checkout' ? ' selected="selected"' : '').'>' . JText::translate('VBRELEASEAT') . '</option>
</select>',
'type' => 'select',
'name' => 'datetype'
);
array_push($this->driverFilters, $filter_opt);
// invoice type filter
$filter_opt = array(
'label' => '<label for="einvtype">Show</label>',
'html' => '<select name="einvtype" id="einvtype">
<option value="0">All reservations</option>
<option value="1"'.($peinvtype == 1 ? ' selected="selected"' : '').'>- To be invoiced</option>
<option value="-1"'.($peinvtype == -1 ? ' selected="selected"' : '').'>- To be transmitted</option>
<option value="-2"'.($peinvtype == -2 ? ' selected="selected"' : '').'>- Trasmitted</option>
</select>',
'type' => 'select',
'name' => 'einvtype'
);
array_push($this->driverFilters, $filter_opt);
// search invoice filter
$filter_opt = array(
'label' => '<label for="einvkword">' . JText::translate('VBODASHSEARCHKEYS') . '</label>',
'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>',
'type' => 'text',
'name' => 'einvkword'
);
array_push($this->driverFilters, $filter_opt);
return $this->driverFilters;
}
/**
* Whether there are enough filters in the session to render data when the page loads.
*
* @return boolean
*/
public function hasFiltersSet()
{
return (bool)(count($this->loadSessionFilters()) > 0);
}
/**
* Returns the current filters saved in the session.
* This protected method is only used by this class.
*
* @return array
*/
protected function loadSessionFilters()
{
if ($this->sessionFilters) {
return $this->sessionFilters;
}
$session = JFactory::getSession();
$sessfilters = $session->get($this->getFileName().'Filt', '');
$sessfilters = empty($sessfilters) || !is_array($sessfilters) ? array() : $sessfilters;
$this->sessionFilters = $sessfilters;
return $this->sessionFilters;
}
/**
* Returns the current session filter for the given name.
* This protected method is only used by this class.
*
* @param string the name of the filter to fetch
* @param mixed the default filter value if empty
*
* @return mixed the current session filter requested, or a default empty value
*/
protected function getSessionFilter($name, $def = '')
{
if (isset($this->sessionFilters[$name])) {
return $this->sessionFilters[$name];
}
return $def;
}
/**
* Sets and updates the session filters.
*
* @param string the name of the filter to set
* @param mixed the value to set for the filter
*
* @return void
*/
protected function setSessionFilter($name, $val)
{
$this->sessionFilters[$name] = $val;
// update session
$session = JFactory::getSession();
$sessfilters = $session->set($this->getFileName().'Filt', $this->sessionFilters);
return;
}
/**
* Returns the buttons for the driver actions.
*
* @return array
*/
public function getButtons()
{
// generate invoices button
array_push($this->driverButtons, '
<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>
');
// transmit invoices button
array_push($this->driverButtons, '
<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>
');
// download invoices button
array_push($this->driverButtons, '
<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>
');
return $this->driverButtons;
}
/**
* Prepares the data for saving the driver settings.
* Validate post vars to make sure they are correct.
*
* @return stdClass
*/
protected function prepareSavingSettings()
{
$data = new stdClass;
$params = new stdClass;
// settings vars
$automatic = VikRequest::getInt('automatic', 0, 'request');
$progcount = VikRequest::getInt('progcount', 1, 'request');
$invoiceinum = VikRequest::getInt('invoiceinum', 1, 'request');
$invoiceinum = $invoiceinum < 1 ? 1 : $invoiceinum;
// we lower the next invoice num because VikBooking::getNextInvoiceNumber() returns increased by 1
$invoiceinum--;
$einvdttype = VikRequest::getString('einvdttype', 'today', 'request');
$einvexnumdt = VikRequest::getString('einvexnumdt', 'new', 'request');
$einvtypecode = VikRequest::getString('einvtypecode', '1.1', 'request');
$vat_exempt_cat = VikRequest::getString('vat_exempt_cat', '1', 'request');
$einv_paymethod = VikRequest::getString('einv_paymethod', '1', 'request');
$einv_inc_class_type = VikRequest::getString('einv_inc_class_type', '', 'request');
$einv_inc_class_cat = VikRequest::getString('einv_inc_class_cat', '', 'request');
$schema_validate = VikRequest::getInt('schema_validate', 0, 'request');
$aade_user_id = VikRequest::getString('aade_user_id', '', 'request');
$aade_subscription_key = VikRequest::getString('aade_subscription_key', '', 'request');
$test_mode = VikRequest::getInt('test_mode', 0, 'request');
$mydata_endpoint_url = VikRequest::getString('mydata_endpoint_url', '', 'request');
$companyname = VikRequest::getString('companyname', '', 'request');
$vatid = VikRequest::getString('vatid', '', 'request');
$country = VikRequest::getString('country', '', 'request');
$address = VikRequest::getString('address', '', 'request');
$streetnumber = VikRequest::getString('streetnumber', '', 'request');
$zip = VikRequest::getString('zip', '', 'request');
$city = VikRequest::getString('city', '', 'request');
// fields validation
$mandatory = [
$companyname,
$vatid,
$country,
$address,
$streetnumber,
$zip,
$city,
$aade_user_id,
$aade_subscription_key,
];
foreach ($mandatory as $field) {
if (empty($field)) {
$this->setError(JText::translate('VBO_PLEASE_FILL_FIELDS'));
return false;
}
}
// update the global configuration setting 'invoiceinum'
$q = "UPDATE `#__vikbooking_config` SET `setting`=".$this->dbo->quote((string)$invoiceinum)." WHERE `param`='invoiceinum';";
$this->dbo->setQuery($q);
$this->dbo->execute();
// build data for saving
$params->einvdttype = $einvdttype;
$params->einvexnumdt = $einvexnumdt;
$params->einvtypecode = $einvtypecode;
$params->vat_exempt_cat = $vat_exempt_cat;
$params->einv_paymethod = $einv_paymethod;
$params->einv_inc_class_type = $einv_inc_class_type;
$params->einv_inc_class_cat = $einv_inc_class_cat;
$params->schema_validate = $schema_validate;
$params->aade_user_id = $aade_user_id;
$params->aade_subscription_key = $aade_subscription_key;
$params->test_mode = $test_mode;
$params->mydata_endpoint_url = $mydata_endpoint_url;
$params->companyname = $companyname;
$params->vatid = $vatid;
$params->country = $country;
$params->address = $address;
$params->streetnumber = $streetnumber;
$params->zip = $zip;
$params->city = $city;
/**
* Environmental Fee settings
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
$params->environmental_invoice = VikRequest::getInt('environmental_invoice', 0, 'request');
$params->envfeeinvoiceinum = VikRequest::getInt('envfeeinvoiceinum', 1, 'request');
$params->envfeevboid = VikRequest::getInt('envfeevboid', 0, 'request');
$data->driver = $this->getFileName();
$data->params = json_encode($params);
$data->automatic = $automatic;
$data->progcount = $progcount;
return $data;
}
/**
* Gets an array with the default settings.
*
* @return array
*/
protected function getDefaultSettings()
{
return [
'id' => -1,
'driver' => $this->getFileName(),
'params' => array(),
'automatic' => 0
];
}
/**
* Echoes the HTML required for the driver settings form.
*
* @return void
*/
public function printSettings()
{
// load current driver settings
$settings = $this->loadSettings();
if ($settings === false) {
$settings = $this->getDefaultSettings();
/**
* it's the first time we run the driver, so we print a warning message
* with some instructions for generating the invoices and to transmit them.
*/
$this->displayInstructions();
}
/**
* Load and inject all the configured fees within VikBooking to support the environmental fee.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
$this->dbo->setQuery(
$this->dbo->getQuery(true)
->select('*')
->from($this->dbo->qn('#__vikbooking_optionals'))
->where(1)
->andWhere([
$this->dbo->qn('forcesel') . ' = 1',
$this->dbo->qn('is_fee') . ' = 1',
], 'OR')
->order($this->dbo->qn('name') . ' ASC')
);
$settings['mandatory_fees'] = $this->dbo->loadAssocList();
// settings layout file
$fpath = $this->driverHelperPath . 'settings.php';
// load helper file and echo its content
echo $this->loadHelperFile($fpath, $settings);
}
/**
* Sets some warning messages.
*
* @return array
*/
protected function displayInstructions()
{
$this->setWarning('Driver settings not available. Make sure to save your personal myDATA information, or the data transmission will not work.');
$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.');
}
/**
* This method converts each booking array into a matrix with one room-booking per index.
* It also adds information about the customer and the invoices generated for each booking.
*
* @param array $records the array containing the bookings before nesting
*
* @return array
*/
protected function nestBookingsData($records)
{
// to avoid heavy and extra joins, we load all customers for the returned booking ids
$allids = [];
foreach ($records as $b) {
if (!isset($b['customer']) && !in_array($b['id'], $allids)) {
array_push($allids, $b['id']);
}
}
$customers_books = [];
if (count($allids)) {
$q = "SELECT `c`.*,`co`.`idorder`,`cy`.`country_name`,`cy`.`country_2_code` FROM `#__vikbooking_customers` AS `c`
LEFT JOIN `#__vikbooking_customers_orders` `co` ON `c`.`id`=`co`.`idcustomer`
LEFT JOIN `#__vikbooking_countries` AS `cy` ON `c`.`country`=`cy`.`country_3_code`
WHERE `co`.`idorder`".(count($allids) === 1 ? "=".(int)$allids[0] : " IN (".implode(', ', $allids).")").";";
$this->dbo->setQuery($q);
$allcustomers = $this->dbo->loadAssocList();
if ($allcustomers) {
foreach ($allcustomers as $customer) {
$customers_books[$customer['idorder']] = $customer;
}
}
}
// nest records with multiple rooms booked inside sub-array
$bookings = [];
foreach ($records as $v) {
if (!isset($bookings[$v['id']])) {
$bookings[$v['id']] = [];
}
// to avoid heavy joins, we put the customer record onto the first nested room booked
if (!isset($v['customer']) && !$bookings[$v['id']]) {
$v['customer'] = isset($customers_books[$v['id']]) ? $customers_books[$v['id']] : [];
}
// push room sub-array
array_push($bookings[$v['id']], $v);
}
return $bookings;
}
/**
* Loads the bookings from the DB according to the filters set.
* Gathers the information for the electronic invoices generation.
* Sets the columns and rows for the page and commands to be displayed.
* Updates the internal bookings array for any custom action.
*
* @return boolean
*/
public function getBookingsData()
{
if (strlen($this->getError())) {
// other methods may set errors rather than exiting the process, and the View may continue the execution to attempt to render the page.
return false;
}
if (count($this->bookings)) {
// this method may be called by other generation methods, so it's useless to run it twice
return true;
}
$cpin = VikBooking::getCPinIstance();
$customsq = '';
// input fields and other vars
$pdatetype = VikRequest::getString('datetype', $this->getSessionFilter('datetype', 'ts'), 'request');
$peinvtype = VikRequest::getInt('einvtype', 0, 'request');
$peinvkword = VikRequest::getString('einvkword', '', 'request');
$pfromdate = VikRequest::getString('fromdate', '', 'request');
$ptodate = VikRequest::getString('todate', '', 'request');
if (empty($pfromdate) && empty($ptodate)) {
// if both request values are empty, take them from the session
$pfromdate = $this->getSessionFilter('fromdate');
$ptodate = $this->getSessionFilter('todate');
}
$pkrsort = VikRequest::getString('krsort', $this->defaultKeySort, 'request');
$pkrsort = empty($pkrsort) ? $this->defaultKeySort : $pkrsort;
$pkrorder = VikRequest::getString('krorder', $this->defaultKeyOrder, 'request');
$pkrorder = empty($pkrorder) ? $this->defaultKeyOrder : $pkrorder;
$pkrorder = $pkrorder == 'DESC' ? 'DESC' : 'ASC';
$currency_symb = VikBooking::getCurrencySymb();
$df = $this->getDateFormat();
$datesep = VikBooking::getDateSeparator();
if (empty($ptodate)) {
$ptodate = $pfromdate;
}
// get dates timestamps
$from_ts = VikBooking::getDateTimestamp($pfromdate, 0, 0);
$to_ts = VikBooking::getDateTimestamp($ptodate, 23, 59, 59);
if (empty($peinvkword) && (empty($pfromdate) || empty($from_ts) || empty($to_ts) || $from_ts > $to_ts)) {
$this->setError('Please select the dates to filter invoices and reservations.');
return false;
}
// update session filters
$this->setSessionFilter('fromdate', $pfromdate);
$this->setSessionFilter('todate', $ptodate);
$this->setSessionFilter('datetype', $pdatetype);
// query to obtain the records
$records = [];
if (!empty($peinvkword)) {
// search invoice requires a different query
$seekclauses = [];
$maybenum = $this->getOnlyNumbers($peinvkword, true);
$maybevat = $this->getOnlyNumbers($peinvkword);
if (!empty($maybenum)) {
// try to seek for this invoice number
array_push($seekclauses, '`ei`.`number`='.(int)$maybenum);
}
if (!empty($maybevat)) {
// customer vat number
array_push($seekclauses, "`cust`.`vat` LIKE ".$this->dbo->quote("%".$maybevat."%"));
}
// customer company name
array_push($seekclauses, "`cust`.`company` LIKE ".$this->dbo->quote("%".$peinvkword."%"));
// customer full name
array_push($seekclauses, "CONCAT_WS(' ', `cust`.`first_name`, `cust`.`last_name`) LIKE ".$this->dbo->quote("%".$peinvkword."%"));
// customer email
if (strpos($peinvkword, '@') !== false) {
// customer email
array_push($seekclauses, "`cust`.`email`=".$this->dbo->quote($peinvkword));
}
// customer fiscal code
array_push($seekclauses, "`cust`.`fisccode`=".$this->dbo->quote($peinvkword));
// find first the booking IDs with a specific query given the filters
$oidsfound = [];
$q = "SELECT `ei`.`id`,`ei`.`idorder` FROM `#__vikbooking_einvoicing_data` AS `ei` ".
"LEFT JOIN `#__vikbooking_customers` AS `cust` ON `ei`.`idcustomer` = `cust`.`id` ".
"WHERE `ei`.`obliterated`=0 AND (".implode(' OR ', $seekclauses).") ".
"GROUP BY `ei`.`driverid`,`ei`.`number`;";
$this->dbo->setQuery($q);
$results = $this->dbo->loadAssocList();
if (!$results) {
$this->setError('No invoice found with the specified filters');
return false;
}
$mergecustoms = false;
$customsids = [];
foreach ($results as $res) {
if ($res['idorder'] < 0) {
$mergecustoms = true;
array_push($customsids, $res['id']);
}
array_push($oidsfound, $res['idorder']);
}
// we make the same query but by passing the IDs of the bookings found according to the filters
$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`,".
"`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` ".
"FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
"LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
"LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
"LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
"WHERE `o`.`id` IN (".implode(', ', array_unique($oidsfound)).") ".
"ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
// check if we need to merge custom (manual) invoices
if ($mergecustoms) {
$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` ".
"FROM `#__vikbooking_einvoicing_data` AS `ei` ".
"LEFT JOIN `#__vikbooking_invoices` AS `inv` ON `ei`.`idorder`=`inv`.`idorder` ".
"WHERE `ei`.`idorder` < 0 AND `ei`.`obliterated`=0 AND `ei`.`id` IN (".implode(', ', $customsids).");";
}
} else {
// use date filters for the regular query
$mergecustoms = false;
$typeclause = '';
// filter by type
switch ($peinvtype) {
case 1:
$typeclause = '`ei`.`id` IS NULL AND ';
break;
case -1:
$mergecustoms = true;
$typeclause = '`ei`.`id` IS NOT NULL AND `ei`.`transmitted`=0 AND ';
break;
case -2:
$mergecustoms = true;
$typeclause = '`ei`.`id` IS NOT NULL AND `ei`.`transmitted`=1 AND ';
break;
default:
// when no e-invoice type filter set, try to merge custom (manual) invoices
$mergecustoms = true;
break;
}
$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`,".
"`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` ".
"FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
"LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
"LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
"LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
"WHERE ".$typeclause.
"(`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." ".
"ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
// 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`)
if ($mergecustoms) {
$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` ".
"FROM `#__vikbooking_einvoicing_data` AS `ei` ".
"LEFT JOIN `#__vikbooking_invoices` AS `inv` ON `ei`.`idorder`=`inv`.`idorder` ".
"WHERE `ei`.`idorder` < 0 AND `ei`.`obliterated`=0 AND {$typeclause}".
"( (`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 ".
"(`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 ".
/**
* We need to add also the following clause in order to not get multiple records with equal invoice numbers for manual bookings.
*/
"( (`inv`.`created_on`>={$from_ts} AND `inv`.`created_on`<={$to_ts}) OR ".
"(`inv`.`for_date`>={$from_ts} AND `inv`.`for_date`<={$to_ts}) );";
}
}
$this->dbo->setQuery($q);
$records = $this->dbo->loadAssocList();
if (!empty($customsq)) {
// we make a query to fetch the custom (manual) invoices to merge them with the real bookings
$this->dbo->setQuery($customsq);
$custom_records = $this->dbo->loadAssocList();
foreach ($custom_records as $customrec) {
$custom_data = $this->prepareCustomInvoiceData($customrec, $cpin->getCustomerByID($customrec['idcustomer']));
// push the prepared custom invoice array to the global records array
array_push($records, $custom_data[0]);
}
}
if (!$records) {
$this->setError('No reservation or invoice found with the specified filters.');
return false;
}
// nest records with multiple rooms booked inside sub-array
$bookings = $this->nestBookingsData($records);
// define the columns of the page
$this->cols = array(
// id
array(
'key' => 'id',
'sortable' => 1,
'label' => 'ID'
),
// date
array(
'key' => 'ts',
'attr' => array(
'class="center"'
),
'sortable' => 1,
'label' => JText::translate('VBPVIEWORDERSONE')
),
// checkin
array(
'key' => 'checkin',
'sortable' => 1,
'label' => JText::translate('VBPICKUPAT')
),
// checkout
array(
'key' => 'checkout',
'sortable' => 1,
'label' => JText::translate('VBRELEASEAT')
),
// customer
array(
'key' => 'customer',
'sortable' => 1,
'label' => JText::translate('VBOCUSTOMER')
),
// country
array(
'key' => 'country',
'sortable' => 1,
'label' => JText::translate('ORDER_STATE')
),
// city
array(
'key' => 'city',
'attr' => array(
'class="center"'
),
'sortable' => 1,
'label' => JText::translate('ORDER_CITY')
),
// vat
array(
'key' => 'vat',
'attr' => array(
'class="center"'
),
'sortable' => 1,
'label' => JText::translate('VBCUSTOMERCOMPANYVAT')
),
// counterpart company name
array(
'key' => 'company',
'sortable' => 1,
'label' => JText::translate('VBCUSTOMERCOMPANY')
),
// total
array(
'key' => 'tot',
'attr' => array(
'class="center"'
),
'sortable' => 1,
'label' => JText::translate('VBPVIEWORDERSSEVEN')
),
// commands
array(
'key' => 'commands',
'attr' => array(
'class="center"'
),
'label' => ''
),
// action
array(
'key' => 'action',
'attr' => array(
'class="center"'
),
'sortable' => 1,
'label' => JText::translate('VBO_BACKUP_ACTION_LABEL')
),
);
// build the rows of the page
foreach ($bookings as $bk => $gbook) {
$bid = $gbook[0]['id'];
$analog_id = isset($gbook[0]['invid']) && !empty($gbook[0]['invid']) ? $gbook[0]['invid'] : null;
/**
* Manual invoices could have the same number and so negative id order across multiple years.
* For this reason, searching for an invoice by number may display invalid links to the manual
* invoices, and so we build a list of invoice IDs with related dates to be displayed.
*/
$multi_analog_ids = [];
if (!empty($analog_id) && count($gbook) > 1) {
$all_analog_ids = [];
foreach ($gbook as $subinv) {
if (!isset($subinv['invid']) || !isset($subinv['inv_fordate_ts'])) {
continue;
}
$inv_key_identifier = $subinv['invid'] . $subinv['inv_fordate_ts'];
if (in_array($inv_key_identifier, $all_analog_ids)) {
continue;
}
array_push($all_analog_ids, $inv_key_identifier);
array_push($multi_analog_ids, array(
'invid' => $subinv['invid'],
'for_date' => date(str_replace("/", $datesep, $df), $subinv['inv_fordate_ts']),
));
}
}
//
$tsinfo = getdate($gbook[0]['ts']);
$tswday = $this->getWdayString($tsinfo['wday'], 'short');
$ininfo = getdate($gbook[0]['checkin']);
$inwday = $this->getWdayString($ininfo['wday'], 'short');
$outinfo = getdate($gbook[0]['checkout']);
$outwday = $this->getWdayString($outinfo['wday'], 'short');
$customer = $gbook[0]['customer'];
$country3 = $gbook[0]['country'];
$country2 = $gbook[0]['country_2_code'];
$countryfull = $gbook[0]['country_name'];
if (empty($country3) && count($customer) && !empty($customer['country'])) {
$country3 = $customer['country'];
$gbook[0]['country'] = $country3;
}
if (empty($country2) && count($customer) && !empty($customer['country_2_code'])) {
$country2 = $customer['country_2_code'];
$gbook[0]['country_2_code'] = $country2;
}
if (empty($countryfull) && count($customer) && !empty($customer['country_name'])) {
$countryfull = $customer['country_name'];
$gbook[0]['country_name'] = $countryfull;
}
$totguests = 0;
$rooms_map = [];
$rooms_str = [];
foreach ($gbook as $book) {
$totguests += $book['adults'] + $book['children'];
if (!isset($book['room_name'])) {
// custom (manual) invoice records may be missing this property
continue;
}
if (!isset($rooms_map[$book['room_name']])) {
$rooms_map[$book['room_name']] = 0;
}
$rooms_map[$book['room_name']]++;
}
foreach ($rooms_map as $rname => $rcount) {
array_push($rooms_str, $rname . ($rcount > 1 ? ' x'.$rcount : ''));
}
$rooms_str = implode(', ', $rooms_str);
// einvnum (if exists)
$einvnum = !empty($gbook[0]['einvnum']) ? $gbook[0]['einvnum'] : 0;
// always update the main array reference
$bookings[$bk] = $gbook;
// check whether the invoice can be issued
list($canbeinvoiced, $noinvoicereason) = $this->canBookingBeInvoiced($bookings[$bk]);
// push fields in the rows array as a new row
array_push($this->rows, array(
array(
'key' => 'id',
'callback' => function ($val) use ($analog_id, $multi_analog_ids) {
if ($val < 0 && !empty($analog_id)) {
// custom (manual) invoices have a negative idorder (-number)
$returi = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
if (count($multi_analog_ids) < 2) {
// just one manual invoice found
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>';
}
/**
* There can be conflictual manual invoices with the same number and negative order
* across multiple years, so we print a link to display them all with an alert.
* @since 1.13.5
*/
$all_links = [];
foreach ($multi_analog_ids as $analog_info) {
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>');
}
return implode('<br/>', $all_links);
}
return '<a href="index.php?option=com_vikbooking&task=editorder&cid[]='.$val.'" target="_blank"><i class="'.VikBookingIcons::i('external-link').'"></i> '.$val.'</a>';
},
'value' => $bid
),
array(
'key' => 'ts',
'attr' => array(
'class="center"'
),
'callback' => function ($val) use ($df, $datesep, $tswday) {
return $tswday.', '.date(str_replace("/", $datesep, $df), $val);
},
'value' => $gbook[0]['ts']
),
array(
'key' => 'checkin',
'callback' => function ($val) use ($df, $datesep, $inwday) {
if (empty($val)) {
// custom (manual) invoices have an empty timestamp
return '-----';
}
return $inwday.', '.date(str_replace("/", $datesep, $df), $val);
},
'value' => $gbook[0]['checkin']
),
array(
'key' => 'checkout',
'callback' => function ($val) use ($df, $datesep, $outwday) {
if (empty($val)) {
// custom (manual) invoices have an empty timestamp
return '-----';
}
return $outwday.', '.date(str_replace("/", $datesep, $df), $val);
},
'value' => $gbook[0]['checkout']
),
array(
'key' => 'customer',
'callback' => function ($val) use ($customer, $bid) {
$goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
if (!empty($val)) {
$cont = count($customer) ? '<a href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">'.$val.'</a>' : $val;
if (count($customer) && !empty($customer['country'])) {
if (is_file(VBO_ADMIN_PATH.DS.'resources'.DS.'countries'.DS.$customer['country'].'.png')) {
$cont .= '<img src="'.VBO_ADMIN_URI.'resources/countries/'.$customer['country'].'.png'.'" title="'.$customer['country'].'" class="vbo-country-flag vbo-country-flag-left"/>';
}
}
} else {
// if empty customer ($val) print danger button to assign a customer to this booking ID
$cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=newcustomer&bid='.$bid.'&goto='.$goto.'">' . JText::translate('VBOCREATENEWCUST') . '</a>';
}
return $cont;
},
'value' => (count($customer) ? $customer['first_name'].' '.$customer['last_name'] : '')
),
array(
'key' => 'country',
'callback' => function ($val) {
return !empty($val) ? $val : '-----';
},
'value' => $countryfull
),
array(
'key' => 'city',
'attr' => array(
'class="center"'
),
'callback' => function ($val) use ($customer) {
$goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
if (empty($val)) {
if (count($customer) && !empty($customer['id'])) {
// just an empty City, edit the customer
$cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
} else {
$cont = '-----';
}
return $cont;
}
if (count($customer) && empty($customer['zip'])) {
// postal code is mandatory
return '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">No Postal Code</a>';
}
if (count($customer) && empty($customer['address'])) {
// address is mandatory
return '<a class="btn btn-secondary" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">No Address</a>';
}
return $val;
},
'value' => (count($customer) && !empty($customer['city']) ? $customer['city'] : '')
),
array(
'key' => 'vat',
'attr' => array(
'class="center"'
),
'callback' => function ($val) use ($customer, $bid) {
if (!empty($val)) {
$cont = $val;
} else {
$goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
if (count($customer) && !empty($customer['id'])) {
// empty VAT Number, which is mandatory for both issuer and counterpart
$cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
} else {
// if empty customer ($val) print danger button to assign a customer to this booking ID
$cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=newcustomer&bid='.$bid.'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
}
}
return $cont;
},
'value' => (count($customer) && !empty($customer['vat']) ? $customer['vat'] : '')
),
array(
'key' => 'company',
'callback' => function ($val) use ($customer) {
$cont = !empty($val) ? $val : '-----';
if (count($customer)) {
$goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
$cont = '<a href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">'.$cont.'</a>';
}
return $cont;
},
'value' => (count($customer) && !empty($customer['company']) ? $customer['company'] : '')
),
array(
'key' => 'tot',
'attr' => array(
'class="center"'
),
'callback' => function ($val) use ($currency_symb) {
return $currency_symb.' '.VikBooking::numberFormat($val);
},
'value' => $gbook[0]['total']
),
array(
'key' => 'commands',
'attr' => array(
'class="center"'
),
'callback' => function ($val) use ($bid, $noinvoicereason) {
if ($val === 0 || $val === 1) {
// invoice cannot be issued or is about to be issued
return '';
}
$buttons = [];
if ($val === -1 || $val === -2) {
// invoice generated or generated and transmitted
array_push($buttons, '<i class="vboicn-eye icn-nomargin vbo-driver-customoutput vbo-driver-output-vieweinv" title="View invoice" data-einvid="' . $noinvoicereason . '"></i>');
array_push($buttons, '<i class="vboicn-pencil2 icn-nomargin vbo-driver-customoutput vbo-driver-output-editeinv" title="Edit invoice" data-einvid="' . $noinvoicereason . '"></i>');
$correlated_inv_numb = $this->getPreviousCorrelatedInvoiceData($noinvoicereason, $bid);
if ($correlated_inv_numb) {
array_push($buttons, '<i class="vboicn-pencil2 icn-nomargin vbo-driver-customoutput vbo-driver-output-editeinv" title="Edit environmental fee invoice" data-einvid="' . $noinvoicereason . '" data-envfeebid="' . $bid . '"></i>');
}
array_push($buttons, '<i class="vboicn-bin icn-nomargin vbo-driver-customoutput vbo-driver-output-rmeinv" title="Delete invoice" data-einvid="' . $noinvoicereason . '"></i>');
}
return implode("\n", $buttons);
},
'value' => $canbeinvoiced
),
array(
'key' => 'action',
'attr' => array(
'class="center vbo-einvoicing-cellaction"',
'data-einvaction="'.$canbeinvoiced.'"'
),
'callback' => function ($val) use ($bid, $noinvoicereason, $einvnum) {
if ($val === 0) {
// invoice cannot be issued
$noinvoicereason = empty($noinvoicereason) ? 'Missing data to generate the invoice' : $noinvoicereason;
return '<button type="button" class="btn btn-secondary" onclick="alert(\''.addslashes($noinvoicereason).'\');"><i class="vboicn-blocked icn-nomargin"></i> Not billable</button>';
}
if ($val === -1) {
// e-invoice already issued and transmitted: print drop down to let the customer regenerate this invoice and obliterate the other or to re-send
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>';
}
if ($val === -2) {
// e-invoice already issued but NOT transmitted: print drop down to let the customer regenerate this invoice and obliterate the other
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>';
}
// invoice can be issued: print drop down to let the customer skip this generation
return '<select class="vbo-einvoicing-selaction" data-bid="'.$bid.'"><option value="0">Generate invoice</option><option value="1">- Do NOT generate invoice</option></select>';
},
'value' => $canbeinvoiced
),
));
}
// sort rows
$this->sortRows($pkrsort, $pkrorder);
// build footer rows
$totcols = count($this->cols);
$footerstats = [];
foreach ($this->rows as $k => $row) {
foreach ($row as $col) {
if ($col['key'] != 'action') {
continue;
}
if (!isset($footerstats[$col['value']])) {
$footerstats[$col['value']] = 0;
}
$footerstats[$col['value']]++;
}
}
$avgcolspan = floor($totcols / count($footerstats));
$footercells = [];
foreach ($footerstats as $canbeinvoiced => $tot) {
switch ($canbeinvoiced) {
case 1:
$descr = 'To be invoiced';
break;
case -1:
$descr = 'Transmitted invoices';
break;
case -2:
$descr = 'Generated invoices';
break;
default:
$descr = 'Not billable';
break;
}
array_push($footercells, array(
'attr' => array(
'class="vbo-report-total vbo-driver-total"',
'colspan="'.$avgcolspan.'"'
),
'value' => '<h3>'.$descr.': '.$tot.'</h3>'
));
}
$this->footerRow[0] = $footercells;
$missingcols = $totcols - ($avgcolspan * count($footerstats));
if ($missingcols > 0) {
array_push($this->footerRow[0], array(
'attr' => array(
'class="vbo-report-total vbo-driver-total"',
'colspan="'.$missingcols.'"'
),
'value' => ''
));
}
// update bookings array for the other methods to avoid double executions
$this->bookings = $bookings;
return true;
}
/**
* Checks whether an e-invoice can be issued for this booking.
*
* @param array the booking array with one array-room per array value
*
* @return array to be used with list(): 0 => (int) can be invoiced, 1 => (string) reason message
*
* @see https://www.aade.gr/sites/default/files/2020-04/myDATA%20API%20Documentation%20v0%206b_eng.pdf
*/
protected function canBookingBeInvoiced($booking)
{
if (empty($booking[0]['customer']) || empty($booking[0]['customer']['vat'])) {
// the VAT number is a mandatory field for both issuer and counterpart
return array(0, 'Missing VAT Number');
}
if (empty($booking[0]['customer']) || empty($booking[0]['customer']['country']) || empty($booking[0]['customer']['country_2_code'])) {
return array(0, 'Missing country');
}
if (empty($booking[0]['customer']) || empty($booking[0]['customer']['city'])) {
return array(0, 'Missing City');
}
if (empty($booking[0]['customer']) || empty($booking[0]['customer']['zip'])) {
return array(0, 'Missing Postal Code');
}
// check if an electronic invoice was already issued for this booking ID by this driver
if (!empty($booking[0]['einvid']) && $booking[0]['einvdriver'] == $this->getDriverId()) {
if ($booking[0]['einvsent'] > 0) {
// 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
return array(-1, $booking[0]['einvid']);
}
// 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
return array(-2, $booking[0]['einvid']);
}
return array(1, '');
}
/**
* @inheritDoc
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
public function elaborateBookingDetails(array &$booking, array &$rooms = [])
{
// load driver settings
$settings = $this->loadSettings();
if (!$settings || !$settings['params']) {
return;
}
// make sure the environmental fee invoice generation setting is enabled
if (empty($settings['params']['environmental_invoice']) || empty($settings['params']['envfeevboid'])) {
return;
}
// look for the environmental fee details
$this->environmental_fee_details = [];
// scan the list of room reservation options to find the environmental fee
foreach ($rooms as $k => $or) {
if (empty($or['optionals'])) {
continue;
}
$stepo = explode(";", $or['optionals']);
foreach ($stepo as $roptkey => $oo) {
if (empty($oo)) {
continue;
}
$stept = explode(":", $oo);
if ((int)$stept[0] != (int)$settings['params']['envfeevboid']) {
continue;
}
$this->dbo->setQuery(
$this->dbo->getQuery(true)
->select('*')
->from($this->dbo->qn('#__vikbooking_optionals'))
->where($this->dbo->qn('id') . ' = ' . (int)$settings['params']['envfeevboid'])
, 0, 1);
$environmental_fee = $this->dbo->loadAssoc();
if ($environmental_fee && !$environmental_fee['pcentroom'] && $environmental_fee['cost']) {
// we've found what we needed, calculate the fee cost
$fee_cost = $environmental_fee['perday'] ? ($environmental_fee['cost'] * $booking['days']) : $environmental_fee['cost'];
if ($environmental_fee['perperson']) {
$fee_cost = $fee_cost * $or['adults'];
}
if (!$this->environmental_fee_details) {
$this->environmental_fee_details = $environmental_fee;
$this->environmental_fee_details['fee_cost'] = 0;
}
// set final cost
$this->environmental_fee_details['fee_cost'] += $fee_cost;
// unset the extra service from the room reservation record
unset($stepo[$roptkey]);
$rooms[$k]['optionals'] = implode(';', $stepo);
// just one fee per room is supported
break;
}
}
}
if (!$this->environmental_fee_details && !empty($booking['idorderota']) && !empty($booking['channel'])) {
// in case of OTA reservation, scan the list of custom extra services and their type
foreach ($rooms as $k => $or) {
if (empty($or['extracosts'])) {
continue;
}
$extra_costs = json_decode($or['extracosts'], true);
if (!is_array($extra_costs) || !$extra_costs) {
continue;
}
foreach ($extra_costs as $ec_key => $ec_data) {
if (!$ec_data || empty($ec_data['type']) || empty($ec_data['name']) || empty($ec_data['cost'])) {
continue;
}
if (!strcasecmp($ec_data['type'], 'env_fee')) {
// environmental fee found from OTA reservation
if (!$this->environmental_fee_details) {
$this->environmental_fee_details = $ec_data;
$this->environmental_fee_details['fee_cost'] = 0;
}
// set final cost
$this->environmental_fee_details['fee_cost'] += (float)$ec_data['cost'];
// unset the custom extra service from the room reservation record
unset($extra_costs[$ec_key]);
if (!$extra_costs) {
$rooms[$k]['extracosts'] = null;
} else {
$rooms[$k]['extracosts'] = json_encode($extra_costs);
}
// just one fee per room is supported
break;
}
}
}
}
if ($this->environmental_fee_details) {
// lower booking total value
$booking['total'] -= $this->environmental_fee_details['fee_cost'];
if (isset($booking['optionals'])) {
$booking['optionals'] = $rooms[0]['optionals'];
}
}
return;
}
/**
* @inheritDoc
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
public function getBookingExtraInvoices($bid)
{
$this->dbo->setQuery(
$this->dbo->getQuery(true)
->select('*')
->from($this->dbo->qn('#__vikbooking_einvoicing_data'))
->where($this->dbo->qn('driverid') . ' = ' . $this->dbo->q($this->getDriverId()))
->where($this->dbo->qn('idorder') . ' = ' . (int)$bid)
->where($this->dbo->qn('transmitted') . ' = 1')
->where($this->dbo->qn('obliterated') . ' = 0')
->order($this->dbo->qn('id') . ' DESC')
, 0, 1);
$last_einvoice = $this->dbo->loadAssoc();
if (!$last_einvoice) {
return [];
}
$booking = VikBooking::getBookingInfoFromID($bid);
// check if a correlated invoice was generated
$correlated_inv_raw_data = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($last_einvoice['id'], $bid), []);
if (!$correlated_inv_raw_data || empty($correlated_inv_raw_data['transmission']) || empty($correlated_inv_raw_data['transmission']['pdf'])) {
return [];
}
// build the PDF locations
$pdffname = implode('_', ['envfee', $booking['id'], ($booking['sid'] ?: $booking['ts'])]) . '.pdf';
$pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
$urlpdf = VBO_SITE_URI . 'helpers/invoices/generated/' . $pdffname;
return [
[
'label' => 'Correlated Invoice',
'path' => $pathpdf,
'uri' => $urlpdf,
]
];
}
/**
* Generates the electronic invoices according to the input parameters.
* This is a 'driver action', and so it's called before getBookingsData()
* in the view. This method will save/update records in the DB so that when
* the view re-calls getBookingsData(), the information will be up to date.
*
* @return boolean True if at least one e-invoice was generated
*/
public function generateEInvoices()
{
// call the main method to generate rows, cols and bookings array
$this->getBookingsData();
if (strlen($this->getError()) || !$this->bookings) {
return false;
}
$generated = 0;
foreach ($this->bookings as $gbook) {
// check whether this booking ID was set to be skipped
$exclude = VikRequest::getInt('excludebid'.$gbook[0]['id'], 0, 'request');
if ($exclude > 0) {
// skipping this invoice
continue;
}
// check if an electronic invoice was already issued for this booking ID by this driver
if (!empty($gbook[0]['einvid']) && $gbook[0]['einvdriver'] == $this->getDriverId()) {
$regenerate = VikRequest::getInt('regeneratebid'.$gbook[0]['id'], 0, 'request');
if (!($regenerate > 0)) {
// we do not re-generate an invoice for this booking ID
continue;
}
}
// generate invoice
if ($this->generateEInvoice($gbook)) {
$generated++;
}
}
// we need to unset the bookings var so that the later call to getBookingsData() made by the View will reload the information
$this->bookings = [];
// unset also cols, rows and footer row to not merge data
$this->cols = [];
$this->rows = [];
$this->footerRow = [];
// set info message
$this->setInfo('Invoices generated: '.$generated);
return ($generated > 0);
}
/**
* Given two arguments, the current analogic invoice record and the customer record, this
* method should prepare and return an array that can be later passed onto generateEInvoice().
* This originally abstract method must be implemented for the generation of the custom (manual) invoices
* that are not related to any bookings (idorder = -number), that were manually created for certain customers.
*
* @param array $invoice the analogic invoice record
* @param array $customer the customer record obtained through VikBookingCustomersPin::getCustomerByID()
*
* @return array the data array compatible with generateEInvoice()
*
* @see generateEInvoice()
*/
public function prepareCustomInvoiceData($invoice, $customer)
{
if (!isset($invoice['number']) && !empty($invoice['einvnum'])) {
// getBookingsData() may call this method by knowing only the electronic invoice number
$invoice['number'] = $invoice['einvnum'];
}
// make sure to get an integer value from the invoice number, which is a string with a probable suffix
$numnumber = intval(preg_replace("/[^\d]+/", '', $invoice['number']));
// make sure the key rawcont is an array
if (!is_array($invoice['rawcont'])) {
$rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : [];
$rawcont = is_array($rawcont) ? $rawcont : [];
$invoice['rawcont'] = $rawcont;
}
// build necessary data array compatible with generateEInvoice()
$data = array(
'id' => ($numnumber - ($numnumber * 2)),
'ts' => (isset($invoice['created_on']) ? strtotime($invoice['created_on']) : time()),
'checkin' => 0,
'checkout' => 0,
'adults' => 0,
'children' => 0,
'total' => $invoice['rawcont']['totaltot'],
'country' => $customer['country'],
'country_name' => $customer['country_name'],
'country_2_code' => (isset($customer['country_2_code']) ? $customer['country_2_code'] : null),
'tot_taxes' => $invoice['rawcont']['totaltax'],
'tot_city_taxes' => 0,
'tot_fees' => 0,
'customer' => $customer,
'pkg' => null,
'einvid' => (isset($invoice['einvid']) ? $invoice['einvid'] : null),
'einvdriver' => (isset($invoice['einvdriver']) ? $invoice['einvdriver'] : null),
'einvdate' => (isset($invoice['einvdate']) ? $invoice['einvdate'] : null),
'einvnum' => (isset($invoice['einvnum']) ? $invoice['einvnum'] : null),
'einvsent' => (isset($invoice['einvsent']) ? $invoice['einvsent'] : null),
// this could be the ID of the analogic invoice
'invid' => (isset($invoice['invid']) ? $invoice['invid'] : null),
// this could be the for date timestamp of the analogic invoice
'inv_fordate_ts' => (isset($invoice['inv_fordate_ts']) ? $invoice['inv_fordate_ts'] : null),
);
// make sure to inject the raw content of the custom invoice
$this->externalData['einvrawcont'] = $invoice['rawcont'];
// original data array contains nested rooms booked so we need to return it as the 0th value
return array($data);
}
/**
* Checks whether an active electronic invoice already exists from the given details.
*
* @param mixed $data array or StdClass object with properties to identify the e-invoice
*
* @return mixed False if the invoice does not exist, its ID otherwise.
*/
public function eInvoiceExists($data)
{
if (is_object($data)) {
// cast to array
$data = (array)$data;
}
// allowed properties to check
$properties = array(
'id' => 'einvid',
'idorder' => 'idorder',
'number' => 'number',
);
$filters = [];
foreach ($properties as $k => $v) {
if (isset($data[$v]) && !empty($data[$v])) {
$filters[$k] = $data[$v];
} elseif (isset($data[$k]) && !empty($data[$k])) {
$filters[$k] = $data[$k];
}
}
if (empty($filters)) {
return false;
}
$clauses = [];
foreach ($filters as $col => $val) {
array_push($clauses, "`{$col}`=".$this->dbo->quote($val));
}
$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;";
$this->dbo->setQuery($q);
$this->dbo->execute();
if (!$this->dbo->getNumRows()) {
return false;
}
return $this->dbo->loadResult();
}
/**
* Attempts to set one e-invoice to obliterated.
*
* @param mixed $data array or StdClass object with properties to identify the e-invoice
*
* @return void
*/
public function obliterateEInvoice($data)
{
if (is_object($data)) {
// cast to array
$data = (array)$data;
}
// allowed properties to check
$properties = array(
'id' => 'einvid',
'idorder' => 'idorder',
'number' => 'number',
);
$filters = [];
foreach ($properties as $k => $v) {
if (isset($data[$v]) && !empty($data[$v])) {
$filters[$k] = $data[$v];
} elseif (isset($data[$k]) && !empty($data[$k])) {
$filters[$k] = $data[$k];
}
}
if (empty($filters)) {
return;
}
$clauses = [];
foreach ($filters as $col => $val) {
array_push($clauses, "`{$col}`=".$this->dbo->quote($val));
}
$q = "UPDATE `#__vikbooking_einvoicing_data` SET `obliterated`=1 WHERE `driverid`=".(int)$this->getDriverId()." AND ".implode(' AND ', $clauses).";";
$this->dbo->setQuery($q);
$this->dbo->execute();
}
/**
* Generates one single electronic invoice. If no array data provided, the booking ID should
* be passed as argument. In this case the method would fetch and nest the booking data.
*
* @param mixed $data either the booking ID or the booking array (one room info per index)
* @param bool $correlated true if the invoice should only contain the environmental fee details.
*
* @return string|bool true if the e-invoice was generated, or string if it was the correlated one.
*/
public function generateEInvoice($data, $correlated = false)
{
// load driver settings
$settings = $this->loadSettings();
if ($settings === false || !$settings['params']) {
$this->setError('Missing driver settings. Please set up the driver first.');
return false;
}
if ($correlated && !$this->environmental_fee_details) {
$this->setError('Could not generate the correlated invoice');
return false;
}
if (is_int($data)) {
// query to obtain the booking records
$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`,".
"`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` ".
"FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
"LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
"LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
"LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
"WHERE ".
"(`o`.`status`='confirmed' OR (`o`.`status`='cancelled' AND `o`.`totpaid`>0)) AND `o`.`closure`=0 AND `o`.`id`=".$this->dbo->quote($data)." ".
"ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
$this->dbo->setQuery($q);
$record = $this->dbo->loadAssocList();
if (!$record) {
$this->setError('Could not find the booking information');
return false;
}
// nest records with multiple rooms booked inside sub-array
$record = $this->nestBookingsData($record);
$data = $record[$data];
}
if (!is_array($data) || empty($data)) {
$this->setError('No bookings found');
return false;
}
/**
* Elaborate the booking details in case of environmental fee available.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
if (!$correlated) {
// build the booking record
$elaborate_booking = $data[0];
// build the room reservation records
$elaborate_rooms = $data;
// elaborate data for the environmental fee
$this->elaborateBookingDetails($elaborate_booking, $elaborate_rooms);
// replace values, all room reservation records first, then the main booking
$data = $elaborate_rooms;
$data[0] = $elaborate_booking;
}
// check whether the invoice can be issued
list($canbeinvoiced, $noinvoicereason) = $this->canBookingBeInvoiced($data);
if ($canbeinvoiced === 0) {
/**
* IMPORTANT: if this method is not called by generateEInvoices(), then the script should
* make sure that an e-invoice is not already available for this booking ID because
* here we skip only if $canbeinvoiced=0 and when e-invoices exist, the code is -1 or -2.
*/
// do not raise any errors unless called externally, we just skip this booking because it cannot be invoiced
if ($this->externalCall) {
if ($data[0]['id'] < 0) {
$message = "Could not generate electronic invoice from custom invoice: {$noinvoicereason}";
} else {
$message = "Could not generate electronic invoice for booking ID {$data[0]['id']} ({$noinvoicereason})";
}
$this->setError($message);
}
return false;
}
// counterpart branch number
$branch = '0';
// invoice/correlated invoice number
$invnum = '';
$correlated_invnum = '';
// counterpart name must not be submitted if entity is from Greece
$client_name = '';
if ((!empty($data[0]['customer']['first_name']) || !empty($data[0]['customer']['last_name'])) && $data[0]['customer']['country'] != 'GRC') {
$client_name = $data[0]['customer']['first_name'] . ' ' . $data[0]['customer']['last_name'];
}
// invoice date and number (suffix not supported for AA serial number)
if (!empty($data[0]['einvnum']) && $settings['params']['einvexnumdt'] == 'old') {
// if an invoice already exists, we re-use the same number also because the setting said so
$invnum = $data[0]['einvnum'];
$invdate = $data[0]['einvdate'];
if ($correlated) {
// get the previous correlated invoice number
$correlated_invnum = $this->getPreviousCorrelatedInvoiceData($data[0]['einvid'], $data[0]['id']);
}
} else {
// get a new invoice number
if ($correlated) {
$correlated_invnum = (int)($settings['params']['envfeeinvoiceinum'] ?: 0) + 1;
} else {
$invnum = VikBooking::getNextInvoiceNumber();
}
$invdate = $settings['params']['einvdttype'] == 'ts' ? date('Y-m-d', $data[0]['ts']) : date('Y-m-d');
}
if (isset($this->externalData['einvnum']) && intval($this->externalData['einvnum']) > 0) {
// external calls may inject the invoice number to use
$invnum = (int)$this->externalData['einvnum'];
}
if (isset($this->externalData['einvdate']) && !empty($this->externalData['einvdate'])) {
// external calls may inject the invoice date to use
$invdate = is_int($this->externalData['einvdate']) ? date('Y-m-d', $this->externalData['einvdate']) : $this->externalData['einvdate'];
}
// invoice series ("in case of non-issuance of series of an invoice, the series field must have a value of 0")
$series = $correlated ? 'C' : '0';
// 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')
$aa_serial_number = $correlated && $correlated_invnum ? $correlated_invnum : $invnum;
// invoice type
$invtype = !empty($settings['params']['einvtypecode']) ? $settings['params']['einvtypecode'] : VikBookingMydataAadeConstants::DEFAULT_INVOICE_TYPE;
$invtype = $correlated ? '8.2' : $invtype;
// invoice total paid amount
$inv_tot_paid = empty($data[0]['totpaid']) ? $data[0]['total'] : $data[0]['totpaid'];
if ($correlated) {
$inv_tot_paid = $this->environmental_fee_details['fee_cost'];
}
// invoice payment method
$inv_pay_method = '';
if (!empty($data[0]['idpayment'])) {
$pay_info_parts = explode('=', $data[0]['idpayment']);
$inv_pay_method = !empty($pay_info_parts[1]) ? $pay_info_parts[1] : $inv_pay_method;
}
// compose the invoice UID
$invoice_uid_parts = [
$settings['params']['vatid'],
$invdate,
$branch,
$invtype,
$series,
$aa_serial_number,
];
$invoice_uid = sha1(implode('', $invoice_uid_parts));
// invoice details and summaries
$invoice_details = [];
$summaries = [];
$summariesvat = [];
$rounded_nets = [];
// whether to include "incomeClassification" nodes
$use_income_classf = (!empty($settings['params']['einv_inc_class_type']) && !empty($settings['params']['einv_inc_class_cat']));
$is_package = (!empty($data[0]['pkg']));
$isdue = 0;
$extralinenum = 0;
$discountval = 0;
if ($data[0]['id'] < 0 && isset($this->externalData['einvrawcont'])) {
// custom (manual) invoice, get the raw content of the invoice
foreach ($this->externalData['einvrawcont']['rows'] as $ind => $row) {
if (!isset($summariesvat[$row['aliq']])) {
$summariesvat[$row['aliq']] = array('net' => 0, 'tax' => 0);
$rounded_nets[$row['aliq']] = 0;
}
$summariesvat[$row['aliq']]['net'] += $row['net'];
$summariesvat[$row['aliq']]['tax'] += $row['tax'];
$rounded_nets[$row['aliq']] += (float)number_format($row['net'], 2, '.', '');
// income classification
$inc_classf_nodes = '';
if ($use_income_classf) {
$inc_classf_nodes = '<incomeClassification>
<N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
<N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
<N1:amount>' . number_format($row['net'], 2, '.', '') . '</N1:amount>
</incomeClassification>';
}
// push invoice details node
$vat_category = VikBookingMydataAadeConstants::getVatCategory($row['aliq']);
array_push($invoice_details, '
<invoiceDetails>
<lineNumber>' . ($ind + 1) . '</lineNumber>
<netValue>' . number_format($row['net'], 2, '.', '') . '</netValue>
<vatCategory>' . $vat_category . '</vatCategory>
<vatAmount>' . number_format($row['tax'], 2, '.', '') . '</vatAmount>
' . ((int)$row['aliq'] === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
<lineComments>'.$this->convertSpecials($row['service']).'</lineComments>
' . $inc_classf_nodes . '
</invoiceDetails>');
}
} else {
// invoice for a regular booking
$tars = $this->getBookingTariffs($data);
// check discount (coupon and/or refund)
$discount_nodes = '';
if (isset($data[0]['coupon']) && strlen($data[0]['coupon']) > 0) {
$expcoupon = explode(";", $data[0]['coupon']);
$discountval += (float)$expcoupon[1];
}
if (isset($data[0]['refund']) && $data[0]['refund'] > 0) {
$discountval += $data[0]['refund'];
}
if ($discountval > 0) {
$discount_nodes = '
<discountOption>true</discountOption>
<deductionsAmount>' . number_format($discountval, 2, '.', '') . '</deductionsAmount>';
}
foreach ($data as $kor => $or) {
$num = $kor + 1;
if ($correlated) {
// push invoice details node
array_push($invoice_details, '
<invoiceDetails>
<lineNumber>1</lineNumber>
<netValue>0.00</netValue>
<vatCategory>8</vatCategory>
<vatAmount>0.00</vatAmount>
<incomeClassification>
<N1:classificationCategory>category1_95</N1:classificationCategory>
<N1:amount>0</N1:amount>
</incomeClassification>
</invoiceDetails>
<taxesTotals>
<taxes>
<taxType>3</taxType>
<taxCategory>10</taxCategory>
<underlyingValue>' . number_format($or['total'], 2, '.', '') . '</underlyingValue>
<taxAmount>' . number_format($this->environmental_fee_details['fee_cost'], 2, '.', '') . '</taxAmount>
</taxes>
</taxesTotals>');
// break the loop for the single environmental fee
break;
}
if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
// package cost or cust_cost may not be inclusive of taxes if prices tax included is off
$descr = $is_package ? sprintf(VikBookingMydataAadeConstants::DESCRPACKAGENIGHTS, $or['days']) : sprintf(VikBookingMydataAadeConstants::DESCRSTAYROOMNIGHTS, $or['days'], strtoupper($or['room_name']));
$cost_minus_tax = VikBooking::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
$cost_tax_amount = (VikBooking::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']) - $cost_minus_tax);
$aliq = $this->getAliquoteById($or['cust_idiva']);
if (!isset($summariesvat[$aliq])) {
$summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
$rounded_nets[$aliq] = 0;
}
$summariesvat[$aliq]['net'] += $cost_minus_tax;
$summariesvat[$aliq]['tax'] += $cost_tax_amount;
$rounded_nets[$aliq] += (float)number_format($cost_minus_tax, 2, '.', '');
// income classification
$inc_classf_nodes = '';
if ($use_income_classf) {
$inc_classf_nodes = '<incomeClassification>
<N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
<N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
<N1:amount>' . number_format($cost_minus_tax, 2, '.', '') . '</N1:amount>
</incomeClassification>';
}
// push invoice details node
array_push($invoice_details, '
<invoiceDetails>
<lineNumber>' . ($num + $extralinenum) . '</lineNumber>
<netValue>' . number_format($cost_minus_tax, 2, '.', '') . '</netValue>
<vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
<vatAmount>' . number_format($cost_tax_amount, 2, '.', '') . '</vatAmount>
' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
' . (($num + $extralinenum) == 1 ? $discount_nodes : '') . '
<lineComments>' . $this->convertSpecials($descr) . '</lineComments>
' . $inc_classf_nodes . '
</invoiceDetails>');
} elseif (isset($tars[$num]) && is_array($tars[$num])) {
// regular tariff
$descr = sprintf(VikBookingMydataAadeConstants::DESCRSTAYROOMNIGHTS, $or['days'], strtoupper($or['room_name']));
$display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
$calctar = VikBooking::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
$aliq = $this->getAliquoteFromPriceId($tars[$num]['idprice']);
$isdue += $calctar;
if ($calctar == $display_rate) {
$cost_minus_tax = VikBooking::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
$tax = ($display_rate - $cost_minus_tax);
} else {
$cost_minus_tax = $display_rate;
$tax = ($calctar - $display_rate);
}
if (!isset($summariesvat[$aliq])) {
$summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
$rounded_nets[$aliq] = 0;
}
$summariesvat[$aliq]['net'] += $cost_minus_tax;
$summariesvat[$aliq]['tax'] += $tax;
$rounded_nets[$aliq] += (float)number_format($cost_minus_tax, 2, '.', '');
// income classification
$inc_classf_nodes = '';
if ($use_income_classf) {
$inc_classf_nodes = '<incomeClassification>
<N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
<N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
<N1:amount>' . number_format($cost_minus_tax, 2, '.', '') . '</N1:amount>
</incomeClassification>';
}
// push invoice details node
array_push($invoice_details, '
<invoiceDetails>
<lineNumber>' . ($num + $extralinenum) . '</lineNumber>
<netValue>' . number_format($cost_minus_tax, 2, '.', '') . '</netValue>
<vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
<vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
' . (($num + $extralinenum) == 1 ? $discount_nodes : '') . '
<lineComments>' . $this->convertSpecials($descr) . '</lineComments>
' . $inc_classf_nodes . '
</invoiceDetails>');
}
// room options
if (!empty($or['optionals']) && !$correlated) {
$stepo = explode(";", $or['optionals']);
foreach ($stepo as $roptkey => $oo) {
if (empty($oo)) {
continue;
}
$stept = explode(":", $oo);
$q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $this->dbo->quote($stept[0]) . ";";
$this->dbo->setQuery($q);
$actopt = $this->dbo->loadAssocList();
if (!$actopt) {
continue;
}
$chvar = '';
if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
$optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
$optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
$optageovrct = VikBooking::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
$child_num = VikBooking::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
$optagecosts = VikBooking::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
$agestept = explode('-', $stept[1]);
$stept[1] = $agestept[0];
$chvar = $agestept[1];
$realcost = 0;
if (!empty($chvar)) {
if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
// percentage value of the adults tariff
if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
$optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
} else {
$display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
$optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
}
} elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
// percentage value of room base cost
if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
$optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
} else {
$display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
$optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
}
}
$actopt[0]['chageintv'] = $chvar;
$actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
$actopt[0]['quan'] = $stept[1];
$realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $or['days'] * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
}
} else {
$actopt[0]['quan'] = $stept[1];
// VBO 1.11 - options percentage cost of the room total fee
if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
$deftar_basecosts = $or['cust_cost'];
} else {
$deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
}
$actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
//
$realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $or['days'] * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
}
if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
$realcost = $actopt[0]['maxprice'];
if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
$realcost = $actopt[0]['maxprice'] * $stept[1];
}
}
if ($actopt[0]['perperson'] == 1) {
$realcost = $realcost * $or['adults'];
}
/**
* Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
*
* @since 1.17.7 (J) - 1.7.7 (WP)
*/
$custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $or, $or]);
if ($custom_calculation) {
$realcost = (float) $custom_calculation[0];
}
$tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
$isdue += $tmpopr;
// increase line number
$extralinenum++;
//
$aliq = $this->getAliquoteById($actopt[0]['idiva']);
if ($tmpopr == $realcost) {
$opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
$tax = ($realcost - $opt_minus_tax);
} else {
$opt_minus_tax = $realcost;
$tax = ($tmpopr - $realcost);
}
$descr = $actopt[0]['is_citytax'] == 1 ? VikBookingMydataAadeConstants::DESCRTOURISTTAX : sprintf(VikBookingMydataAadeConstants::DESCRROOMOPTION, strtoupper($actopt[0]['name']));
if (!isset($summariesvat[$aliq])) {
$summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
$rounded_nets[$aliq] = 0;
}
$summariesvat[$aliq]['net'] += $opt_minus_tax;
$summariesvat[$aliq]['tax'] += $tax;
$rounded_nets[$aliq] += (float)number_format($opt_minus_tax, 2, '.', '');
// income classification
$inc_classf_nodes = '';
if ($use_income_classf) {
$inc_classf_nodes = '<incomeClassification>
<N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
<N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
<N1:amount>' . number_format($opt_minus_tax, 2, '.', '') . '</N1:amount>
</incomeClassification>';
}
// push invoice details node
array_push($invoice_details, '
<invoiceDetails>
<lineNumber>' . ($num + $extralinenum) . '</lineNumber>
<netValue>' . number_format($opt_minus_tax, 2, '.', '') . '</netValue>
<vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
<vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
<lineComments>' . $this->convertSpecials($descr) . '</lineComments>
' . $inc_classf_nodes . '
</invoiceDetails>');
}
}
// custom extra costs
if (!empty($or['extracosts']) && !$correlated) {
$cur_extra_costs = json_decode($or['extracosts'], true);
foreach ($cur_extra_costs as $eck => $ecv) {
// increase line number
$extralinenum++;
//
$ecplustax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
$isdue += $ecplustax;
$descr = sprintf(VikBookingMydataAadeConstants::DESCRROOMEXTRACOST, strtoupper($ecv['name']));
if ($ecplustax == $ecv['cost']) {
$ec_minus_tax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
$tax = ($ecv['cost'] - $ec_minus_tax);
} else {
$ec_minus_tax = ($ecplustax - $ecv['cost']);
$tax = ($ecplustax - $ecv['cost']);
}
$aliq = $this->getAliquoteById($ecv['idtax']);
if (!isset($summariesvat[$aliq])) {
$summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
$rounded_nets[$aliq] = 0;
}
$summariesvat[$aliq]['net'] += $ec_minus_tax;
$summariesvat[$aliq]['tax'] += $tax;
$rounded_nets[$aliq] += (float)number_format($ec_minus_tax, 2, '.', '');
// income classification
$inc_classf_nodes = '';
if ($use_income_classf) {
$inc_classf_nodes = '<incomeClassification>
<N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
<N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
<N1:amount>' . number_format($ec_minus_tax, 2, '.', '') . '</N1:amount>
</incomeClassification>';
}
/**
* Removed "quantity" and "measurementUnit" nodes from every "invoiceDetails" node.
*
* <quantity>1.00</quantity>
* <measurementUnit>' . VikBookingMydataAadeConstants::DEFAULT_MEAS_UNIT . '</measurementUnit>
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
// push invoice details node
array_push($invoice_details, '
<invoiceDetails>
<lineNumber>' . ($num + $extralinenum) . '</lineNumber>
<netValue>' . number_format($ec_minus_tax, 2, '.', '') . '</netValue>
<vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
<vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
<lineComments>' . $this->convertSpecials($descr) . '</lineComments>
' . $inc_classf_nodes . '
</invoiceDetails>');
}
}
}
}
// build riepiloghi IVA
$grand_total_net = 0;
$grand_total_vat = 0;
$grand_total_tax_no_rate = 0;
$environmental_fee_amount = $correlated ? $this->environmental_fee_details['fee_cost'] : 0;
foreach ($summariesvat as $aliq => $vat_summary) {
$totnet = number_format($vat_summary['net'], 2, '.', '');
$tottax = number_format($vat_summary['tax'], 2, '.', '');
if (isset($rounded_nets[$aliq]) && (float)$totnet != $rounded_nets[$aliq]) {
/**
* In case of several rows in the invoice, maybe a lot of Extra Services,
* there can be a discrepancy between the sum of the <netValue> nodes
* in the <invoiceDetails> nodes, and the <taxAmount> in <taxesTotals>.
* We need to prevent the amounts to be different because of number_format and
* adjust the amounts and obtain the same value as the sum of the nets in the lines.
* The issue was reproduced with 9 Extra Services, one Room, one Tourist Tax (Option).
*
* @see sandbox booking ID 1140
*/
if ($rounded_nets[$aliq] > (float)$totnet) {
$diff = $rounded_nets[$aliq] - (float)$totnet;
$totnet = number_format($rounded_nets[$aliq], 2, '.', '');
$tottax = number_format(((float)$tottax - $diff), 2, '.', '');
} else {
$diff = (float)$totnet - $rounded_nets[$aliq];
$totnet = number_format($rounded_nets[$aliq], 2, '.', '');
$tottax = number_format(((float)$tottax + $diff), 2, '.', '');
}
}
// sum grand total values
$grand_total_net += $totnet;
if ((int)$aliq > 0) {
$grand_total_vat += $tottax;
} else {
/**
* @todo are we doing good by summing this kind of tax, which is not VAT because
* the tax rate is 0%, to the "total withheld amount"? Or should we use the
* node <totalOtherTaxesAmount> instead?
*/
$grand_total_tax_no_rate += $tottax;
}
/**
* For the moment we ingore completely the <taxesTotals> node and sub-nodes. Docs say:
* "Field taxesTotals contains all taxes except VAT. If user users this element,
* taxes will not exist in invoiceDetails".
* However, here we have a sum of tax amounts for any aliquote (tax rate) involved.
*
* @todo check if these nodes should be somehow composed even if they are optional.
*/
}
/**
* Address element is forbidden for issuer from Greece.
*/
$issuer_address_nodes = '';
if (strcasecmp($settings['params']['country'], 'GR')) {
// issuer not from Greece, compose address
$issuer_address_nodes = '<address>
<street>' . $this->convertSpecials($settings['params']['address']) . '</street>
<number>' . $settings['params']['streetnumber'] . '</number>
<postalCode>' . $settings['params']['zip'] . '</postalCode>
<city>' . $this->convertSpecials($settings['params']['city']) . '</city>
</address>';
}
// total income classification
$inc_classf_nodes = '';
if ($use_income_classf) {
if ($correlated) {
$inc_classf_nodes = '<incomeClassification>
<N1:classificationCategory>category1_95</N1:classificationCategory>
<N1:amount>' . number_format(0, 2, '.', '') . '</N1:amount>
</incomeClassification>';
} else {
$inc_classf_nodes = '<incomeClassification>
<N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
<N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
<N1:amount>' . number_format($grand_total_net, 2, '.', '') . '</N1:amount>
</incomeClassification>';
}
}
// build XML
$root_namespaces = VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
$xml = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<InvoicesDoc ' . $root_namespaces . '>
<invoice>
<uid>' . $invoice_uid . '</uid>
<mark>' . ($correlated ? $correlated_invnum : $settings['progcount']) . '</mark>
<issuer>
<vatNumber>' . $settings['params']['vatid'] . '</vatNumber>
<country>' . $settings['params']['country'] . '</country>
<branch>0</branch>
' . (strcasecmp($settings['params']['country'], 'GR') ? '<name>' . $this->convertSpecials($settings['params']['companyname']) . '</name>' : '') . '
' . (!empty($issuer_address_nodes) ? $issuer_address_nodes : '') . '
</issuer>
<counterpart>
' . (!empty($data[0]['customer']['vat']) ? '<vatNumber>' . $this->convertSpecials($data[0]['customer']['vat']) . '</vatNumber>' : '') . '
' . (!empty($data[0]['customer']['country_2_code']) ? '<country>' . $this->convertSpecials($data[0]['customer']['country_2_code']) . '</country>' : '') . '
<branch>' . $branch . '</branch>
' . (!empty($client_name) ? '<name>' . $this->convertSpecials($client_name) . '</name>' : '') . '
<address>
<street>' . $this->convertSpecials(preg_replace("/[0-9]/", '', $data[0]['customer']['address'])) . '</street>
<number>' . preg_replace("/[^0-9]/", '', $data[0]['customer']['address']) . '</number>
<postalCode>' . $this->convertSpecials($data[0]['customer']['zip']) . '</postalCode>
<city>' . $this->convertSpecials($data[0]['customer']['city']) . '</city>
</address>
</counterpart>
<invoiceHeader>
<series>' . $series . '</series>
<aa>' . $aa_serial_number . '</aa>
<issueDate>' . $invdate . '</issueDate>
<invoiceType>' . $invtype . '</invoiceType>
<currency>' . VikBooking::getCurrencyName() . '</currency>
' . ($correlated ? '<correlatedInvoices>{main_invoice_mark}</correlatedInvoices>' : '') . '
</invoiceHeader>
<paymentMethods>
<paymentMethodDetails>
<type>' . $settings['params']['einv_paymethod'] . '</type>
<amount>' . number_format($inv_tot_paid, 2, '.', '') . '</amount>
' . (!empty($inv_pay_method) ? '<paymentMethodInfo>' . $this->convertSpecials($inv_pay_method) . '</paymentMethodInfo>' : '') . '
</paymentMethodDetails>
</paymentMethods>
' . implode("\n", $invoice_details) . '
<invoiceSummary>
<totalNetValue>' . number_format($grand_total_net, 2, '.', '') . '</totalNetValue>
<totalVatAmount>' . number_format($grand_total_vat, 2, '.', '') . '</totalVatAmount>
<totalWithheldAmount>' . number_format($grand_total_tax_no_rate, 2, '.', '') . '</totalWithheldAmount>
<totalFeesAmount>0.00</totalFeesAmount>
<totalStampDutyAmount>0.00</totalStampDutyAmount>
<totalOtherTaxesAmount>' . number_format($environmental_fee_amount, 2, '.', '') . '</totalOtherTaxesAmount>
<totalDeductionsAmount>' . number_format(($correlated ? 0 : $discountval), 2, '.', '') . '</totalDeductionsAmount>
<totalGrossValue>' . number_format(($correlated ? $this->environmental_fee_details['fee_cost'] : $data[0]['total']), 2, '.', '') . '</totalGrossValue>
' . $inc_classf_nodes . '
</invoiceSummary>
</invoice>
</InvoicesDoc>';
// attempt to properly format the XML string
$this->formatXmlString($xml);
if ($correlated) {
// update driver setting
$this->updateDriverSetting('envfeeinvoiceinum', $correlated_invnum);
// return the raw XML for the correlated invoice just built
return $xml;
}
// check if we need to validate the XML against the official schema
if (!empty($settings['params']['schema_validate'])) {
/**
* It may not be possible to validate the XML against the schema, as on
* some environments this process may run out of execution time.
*/
try {
$schema_validation = $this->validateXmlAgainstSchema($xml);
if ($schema_validation === null) {
// display warning
$this->setWarning('Missing PHP libraries for DOMDocument to validate the XML invoice against the official schema.');
}
} catch (Exception $e) {
// display warning
$this->setWarning('Could not validate the XML invoice against the official Schema - process failed with no response.');
}
}
if ($this->debugging()) {
$this->setWarning('<pre>'.htmlentities($xml).'</pre><br/>');
// break the process when in debug mode
return false;
}
// we proceed with the generation
// invoice name (transmission date-time string + auto-increment registration value just for our internal purpose)
$einvname = date('YmdHis') . '_' . $settings['progcount'] . '.xml';
// get current datetime object in local format
$date_obj = JFactory::getDate();
$date_obj->setTimezone(new DateTimeZone(date_default_timezone_get()));
// prepare object for storing the invoice
$einvobj = new stdClass;
$einvobj->driverid = $settings['id'];
$einvobj->created_on = $date_obj->toSql($local = true);
$einvobj->for_date = $invdate;
$einvobj->filename = $einvname;
$einvobj->number = $invnum;
$einvobj->idorder = $data[0]['id'];
$einvobj->idcustomer = !empty($data[0]['customer']['id']) ? $data[0]['customer']['id'] : 0;
$einvobj->country = !empty($data[0]['customer']['country']) ? $data[0]['customer']['country'] : null;
// this column is not needed in this driver, but we give it a default value
$einvobj->recipientcode = '';
$einvobj->xml = $xml;
// always reset transmitted and obliterated values for new e-invoices
$einvobj->transmitted = 0;
$einvobj->obliterated = 0;
$newinvid = $this->storeEInvoice($einvobj);
if ($newinvid === false) {
$this->setError('Error storing the electronic invoice for the reservation ID '.$data[0]['id']);
return false;
}
if ($canbeinvoiced < 0) {
// log event history when regenerating an e-invoice
VikBooking::getBookingHistoryInstance()->setBid($data[0]['id'])->store('BI', ($this->getName() . ' #' . $invnum));
}
// update settings before generating the analogic invoice in PDF format to prevent exceptions to be thrown or exit/die calls.
// update configuration setting for VikBooking::getNextInvoiceNumber()
if ($data[0]['id'] > 0) {
// we exclude custom (manual) invoices which would have a booking ID set to -number
$this->updateInvoiceNumber($invnum);
}
// update auto-increment driver setting by increasing it for the next run
$this->updateProgressiveNumber(++$settings['progcount']);
/**
* Check if we should generate another, correlated, invoice.
*/
if (!$correlated && $this->environmental_fee_details) {
// re-call the same method to generate the invoice for the environmental fee
$correlated_inv_xml = $this->generateEInvoice($data, true);
if ($correlated_inv_xml) {
// let the method store the raw XML for the correlated invoice
$this->prepareCorrelatedInvoice($einvobj, $data, $correlated_inv_xml);
}
}
if (!$correlated && !$this->hasAnalogicInvoice($data[0]['id'])) {
// no analogic invoice in PDF available, so we create it
if (!$this->generateAnalogicInvoice($data[0]['id'], $invnum, $invdate)) {
// raise warning in case of error
$this->setWarning('It was not possible to generate the courtesy PDF version of the invoice for the reservation ID '.$data[0]['id']);
}
}
return true;
}
/**
* Builds the param name to read the correlated e-invoice data from the db settings.
*
* @param int $einv_id the e-invoice record ID.
* @param int $booking_id the reservation record ID.
*
* @return string
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
public function getCorrelatedInvoiceParamName($einv_id, $booking_id)
{
$driver_id = $this->getDriverId();
return "envfee_invoice_{$driver_id}_{$einv_id}_{$booking_id}";
}
/**
* Stores a record with the information to create the environmental fee invoice (correlated invoice).
* The actual invoice will be created upon transmitting the main one for the reservation because
* the environmental fee invoice requires the correlated number to be the Mark of the main invoice.
*
* @param object $einvobj the main e-invoice record.
* @param array $data the nested booking record.
* @param string $xml the raw XML generated.
*
* @return bool
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
public function prepareCorrelatedInvoice($einvobj, array $data, $xml)
{
if (!is_object($einvobj) || empty($einvobj->id) || !$data) {
return false;
}
$booking_id = $data[0]['id'];
$einv_id = $einvobj->id;
$driver_id = $this->getDriverId();
$config_param_name = $this->getCorrelatedInvoiceParamName($einv_id, $booking_id);
$config_param_value = [
'bid' => $booking_id,
'einvid' => $einv_id,
'envfee' => $this->environmental_fee_details,
'xml' => $xml,
];
VBOFactory::getConfig()->set($config_param_name, $config_param_value);
return true;
}
/**
* Attempts to get the previous correlated invoice number.
* Useful when re-generating an invoice already transmitted.
*
* @param int $einv_id the main invoice ID.
* @param int $bid the VBO booking ID.
* @param string $type the type of data to fetch.
*
* @return string|array
*/
public function getPreviousCorrelatedInvoiceData($einv_id, $bid, $type = '')
{
$driver_id = $this->getDriverId();
$correlated_inv_raw_data = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einv_id, $bid), []);
if (!$correlated_inv_raw_data || empty($correlated_inv_raw_data['xml'])) {
return '';
}
$xml_inv = simplexml_load_string($correlated_inv_raw_data['xml']);
if (!$xml_inv) {
return '';
}
if (!strcasecmp($type, 'date')) {
// previous invoice date
return (string)$xml_inv->invoice->invoiceHeader->issueDate;
}
if (!strcasecmp($type, 'xml')) {
// return the plain XML
return $correlated_inv_raw_data['xml'];
}
if (!strcasecmp($type, 'record')) {
// return the whole array record
return $correlated_inv_raw_data;
}
if (!strcasecmp($type, 'transmission')) {
// return the whole transmission array, if available
return $correlated_inv_raw_data['transmission'] ?? [];
}
// default to previous invoice number
return (string)$xml_inv->invoice->mark;
}
/**
* Manipulates the previously created correlated invoice by adding the proper invoice mark,
* then transmits the second e-invoice to myDATA.
*
* @param object $main_invoice_data the main e-invoice transaction information object.
* @param array $env_fee_data the prepared data for the correlated invoice and fee.
* @param array $extras associative array to pass extra information.
*
* @return bool
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
public function transmitCorrelatedInvoice($main_invoice_data, array $env_fee_data, array $extras)
{
// first off, set the proper correlated invoice number by using the mark
// we use a regex that will capture two groups to avoid problems with the number-value for replacement
$correlated_einv_final_xml = preg_replace("/<(correlatedInvoices)>(\{[a-z_]*\})?<\/correlatedInvoices>/i", '<$1>' . $main_invoice_data->invoice_mark . '</$1>', $env_fee_data['xml']);
// transmit the XML correlated e-invoice to myDATA
$response = $this->myDATARequestPOST('SendInvoices', $correlated_einv_final_xml);
if ($response->code != 200) {
// the request was not successful, and the XML invoices were not parsed at all by myDATA
$this->setError(sprintf('Correlated e-invoice - Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
$this->setError('Correlated e-invoice - Could not send the invoice to myDATA.');
return false;
}
// process the transmission response
$res_obj = simplexml_load_string($response->body);
if (!is_object($res_obj) || !isset($res_obj->response)) {
$this->setError('Could not parse XML response');
$this->setError('<pre>' . htmlentities($response->body) . '</pre>');
return false;
}
if (!isset($res_obj->response->statusCode)) {
$this->setError('Unexpected nodes in XML response (missing statusCode)');
$this->setError('<pre>' . htmlentities($response->body) . '</pre>');
return false;
}
// check if we have a successful status code for this invoice
if (!strcasecmp((string)$res_obj->response->statusCode, 'Success')) {
// get the invoice UID
$invoice_uid = isset($res_obj->response->invoiceUid) ? (string)$res_obj->response->invoiceUid : null;
// get the invoice mark (needed for a later cancellation)
$invoice_mark = isset($res_obj->response->invoiceMark) ? (string)$res_obj->response->invoiceMark : null;
// get the invoice QRCode URL
$invoice_qrcode = isset($res_obj->response->qrUrl) ? (string)$res_obj->response->qrUrl : null;
$invoice_qrcode = !isset($res_obj->response->qrUrl) && isset($res_obj->response->qrCodeUrl) ? (string)$res_obj->response->qrCodeUrl : $invoice_qrcode;
// prepare data transmission values to be updated
$config_param_name = $this->getCorrelatedInvoiceParamName($extras['einvid'], $extras['bid']);
$env_fee_data['xml'] = $correlated_einv_final_xml;
$env_fee_data['transmission'] = [
'ts' => time(),
'uid' => $invoice_uid,
'mark' => $invoice_mark,
'qrurl' => $invoice_qrcode,
'qrcode_img' => '',
'pdf' => '',
];
if ($invoice_qrcode) {
// generate the QR Code for the environmental fee invoice as well
// the QR Code PNG file name
$filename = "aade_qrcode_env_{$extras['bid']}_{$extras['einvid']}.png";
if ($this->generateQRCodeImage($invoice_qrcode, VikBookingMydataAadeConstants::getQRCodeBase('path', $filename))) {
// set the QR Code image property
$env_fee_data['transmission']['qrcode_img'] = $filename;
}
}
// immediately update data transmission values before generating the PDF invoice
VBOFactory::getConfig()->set($config_param_name, $env_fee_data);
// generate the PDF (courtesy) for the correlated invoice (will set the PDF path in case of success)
if ($this->generateAnalogicEnvFeeInvoice($env_fee_data)) {
// update data transmission values again, as they will contain the path to the PDF file
VBOFactory::getConfig()->set($config_param_name, $env_fee_data);
}
return true;
}
// at this point we expect an error
if (!isset($res_obj->response->errors) || !isset($res_obj->response->errors->error)) {
// errors should be set, but if they aren't, this is unexpected
$this->setError('Unexpected nodes in XML response (missing errors or error)');
$this->setError('<pre>' . htmlentities($response->body) . '</pre>');
return false;
}
// loop through the errors
foreach ($res_obj->response->errors->error as $resp_err) {
$err_code = isset($resp_err->code) ? (string)$resp_err->code : '0';
$err_mess = isset($resp_err->message) ? (string)$resp_err->message : '???';
$this->setError(sprintf('Error (%s): %s', $err_code, $err_mess));
}
return false;
}
/**
* Generates a PDF file for the correlated invoice for the environmental fee.
*
* @param array &$env_fee_data the raw environmental fee information data.
*
* @return bool
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
public function generateAnalogicEnvFeeInvoice(&$env_fee_data)
{
// get the customer information
$customer = VikBooking::getCPinInstance()->getCustomerFromBooking($env_fee_data['bid']);
// build a dummy invoice associative array with the information required
$invoice = [
'id' => -1,
'number' => $this->getPreviousCorrelatedInvoiceData($env_fee_data['einvid'], $env_fee_data['bid']),
'for_date' => strtotime($this->getPreviousCorrelatedInvoiceData($env_fee_data['einvid'], $env_fee_data['bid'], 'date')),
'rawcont' => [
'totalnet' => 0,
'totaltax' => $env_fee_data['envfee']['fee_cost'],
'totaltot' => $env_fee_data['envfee']['fee_cost'],
'rows' => [
[
'service' => $env_fee_data['envfee']['name'],
'net' => 0,
'tax' => $env_fee_data['envfee']['fee_cost'],
'tot' => $env_fee_data['envfee']['fee_cost'],
],
],
],
'env_fee_data' => $env_fee_data,
'feeseries' => 'C',
];
// load the custom invoice template file
list($invoice_tmpl, $pdfparams) = VikBooking::loadCustomInvoiceTmpl($invoice, $customer);
// trigger an event to allow third-party plugins to manipulate the content of the custom invoice
VBOFactory::getPlatform()->getDispatcher()->trigger('onMydataBeforeGenerateEnvFeeCourtesyInvoice', [$env_fee_data, $invoice, $invoice_tmpl]);
// parse the content of the template file
$invoice_body = VikBooking::parseCustomInvoiceTemplate($invoice_tmpl, $invoice, $customer);
// reload booking details
$booking_details = VikBooking::getBookingInfoFromID($env_fee_data['bid']);
// force the execution of the conditional text rules
VikBooking::getConditionalRulesInstance()
->set(
[
'booking',
'rooms',
],
[
$booking_details,
VikBooking::loadOrdersRoomsData($env_fee_data['bid']),
]
)
->parseTokens($invoice_body);
// load dependencies
if (!class_exists('TCPDF')) {
require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
}
$usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
/**
* Trigger event to allow third party plugins to return a specific font name.
*/
$custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
$usepdffont = $custom_pdf_font[0];
}
// write the PDF on file
$pdffname = implode('_', ['envfee', $booking_details['id'], ($booking_details['sid'] ?: $booking_details['ts'])]) . '.pdf';
$pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
if (is_file($pathpdf)) {
@unlink($pathpdf);
}
$pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
$pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
$pdf->SetTitle(JText::translate('VBOINVNUM') . ' ' . $invoice['number']);
// header for each page of the pdf
if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
$pdf->SetHeaderData($pdfparams['header_data'][0], $pdfparams['header_data'][1], $pdfparams['header_data'][2], $pdfparams['header_data'][3], $pdfparams['header_data'][4], $pdfparams['header_data'][5]);
}
// change some currencies to their unicode (decimal) value
$currencyname = VikBooking::getCurrencyName();
$unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
if (array_key_exists($currencyname, $unichr_map)) {
$invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
}
// header and footer fonts
$pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
$pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
// margins
$pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
$pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
$pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
$pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
$pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
$pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
$pdf->SetPrintHeader(false);
}
if ($pdfparams['show_footer'] == 0) {
$pdf->SetPrintFooter(false);
}
$pdf->AddPage();
$pdf->writeHTML($invoice_body, true, false, true, false, '');
$pdf->lastPage();
$pdf->Output($pathpdf, 'F');
if (!is_file($pathpdf)) {
return false;
}
if (VBOPlatformDetection::isWordPress()) {
/**
* @wponly - trigger files mirroring
*/
VikBookingLoader::import('update.manager');
VikBookingUpdateManager::triggerUploadBackup($pathpdf);
}
// set the PDF file name at last
$env_fee_data['transmission']['pdf'] = $pdffname;
return true;
}
/**
* Returns the calculated tariffs given their IDs per room booked.
*
* @param array $booking the booking array with one array-room per array value
*
* @return array associative array of tariffs for each room booked
*/
protected function getBookingTariffs($booking)
{
$tars = [];
$is_package = (!empty($booking[0]['pkg']));
foreach ($booking as $kor => $or) {
$num = $kor + 1;
if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
// package or custom cost set from the back-end does not need calculation
continue;
}
$q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=".(int)$or['idtar'].";";
$this->dbo->setQuery($q);
$tar = $this->dbo->loadAssocList();
if ($tar) {
$tar = VikBooking::applySeasonsRoom($tar, $or['checkin'], $or['checkout']);
// apply OBP rules
$tar = VBORoomHelper::getInstance()->applyOBPRules($tar, $or, $or['adults']);
$tars[$num] = $tar[0];
}
}
return $tars;
}
/**
* Transmits the electronic invoices to myDATA according to the input parameters.
* This is a 'driver action', and so it's called before getBookingsData()
* in the view. This method will save/update records in the DB so that when
* the view re-calls getBookingsData(), the information will be up to date.
*
* @return boolean True if at least one e-invoice was transmitted
*/
public function transmitEInvoices()
{
// make sure the transmission settings are not empty
$settings = $this->loadSettings();
if ($settings === false || !$settings['params']) {
$this->setError('Missing settings to transmit the invoices. Please set up the driver settings first.');
return false;
}
// make sure the settings we need are not empty
$required = [
$settings['params']['aade_user_id'],
$settings['params']['aade_subscription_key'],
];
foreach ($required as $reqset) {
if (empty($reqset)) {
$this->setError('Invalid settings to transmit the invoices. Please make sure to provide all the information from the driver settings.');
return false;
}
}
// call the main method to generate rows, cols and bookings array
$this->getBookingsData();
if ($this->getError() || !$this->bookings) {
return false;
}
// get the driver ID
$driver_id = $this->getDriverId();
// pool of e-invoice IDs to transmit
$einvspool = [];
$einvnumbs = [];
// electronic invoices IDs referenced to booking IDs
$einvs_bids_ref = [];
foreach ($this->bookings as $gbook) {
// check whether this booking ID was set to be skipped from transmission
$exclude = VikRequest::getInt('excludesendbid'.$gbook[0]['id'], 0, 'request');
if ($exclude > 0) {
// skipping this invoice from transmission
continue;
}
// make sure an electronic invoice was already issued for this booking ID by this driver
if (empty($gbook[0]['einvid']) || $gbook[0]['einvdriver'] != $this->getDriverId()) {
// no e-invoices available for this booking, skipping
continue;
}
// check if an e-invoice was already sent for this booking
if ($gbook[0]['einvsent'] > 0) {
$resend = VikRequest::getInt('resendbid'.$gbook[0]['id'], 0, 'request');
if (!($resend > 0)) {
// we do not re-send the invoice for this booking ID
continue;
}
}
// push e-invoice ID to the pool
array_push($einvspool, $gbook[0]['einvid']);
// push also the corresponding invoice number
array_push($einvnumbs, $gbook[0]['einvnum']);
// set the e-invoice ID/booking ID relation
$einvs_bids_ref[$gbook[0]['einvid']] = $gbook[0]['id'];
}
if (!$einvspool) {
// no e-invoices generated or ready to be transmitted
$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.');
return false;
}
// build one XML file for all XML e-invoices (if more than one)
$einv_xml_body = $this->buildTransmissionXMLBody($einvspool, $settings);
if ($einv_xml_body === false) {
// something went wrong with the creation of the XML file
$this->setError('Error creating the XML file for the request. Unable to proceed.');
return false;
}
if ($this->debugging()) {
// when in debug mode, the raw XML request is sent to output
$this->setWarning('Raw XML request for Debug Mode');
$this->setWarning('<pre> ' . htmlentities($einv_xml_body) . ' </pre>');
}
// transmit e-invoices to myDATA
$response = $this->myDATARequestPOST('SendInvoices', $einv_xml_body, $settings);
if ($response->code != 200) {
// the request was not successful, and the XML invoices were not parsed at all by myDATA
$this->setError(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
$this->setError('Could not send the invoice(s) to myDATA.');
return false;
}
if ($this->debugging()) {
// when in debug mode, the raw XML response is sent to output
$this->setWarning('Raw XML response for Debug Mode');
$this->setWarning('<pre> ' . htmlentities($response->body) . ' </pre>');
}
// check if the XML response contains errors, and adjust the e-invoices that succeeded
list($success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls) = $this->myDATAParseXMLResponse($response->body, $einvspool, $einvnumbs);
if (!$success) {
// some errors occurred
if (!is_array($valid_einv_marks) || !$valid_einv_marks) {
$this->setError('Could not send the invoice(s) to myDATA.');
return false;
} else {
// some e-invoices were transmitted successfully
$einvspool = array_keys($valid_einv_marks);
}
}
// update ProgressivoInvio driver setting by increasing it for the next run
$this->updateProgressiveNumber(++$settings['progcount']);
// set to transmitted=1 all e-invoice IDs that were transmitted with success
foreach ($einvspool as $einvid) {
// find the corresponding booking ID
$einv_bid = $einvs_bids_ref[$einvid] ?? 0;
// flag to check if the PDF invoice should be refreshed
$qrcode_fname = null;
// prepare "transmission data" object
$trans_data = new stdClass;
$trans_data->invoice_uid = (isset($valid_einv_uids[$einvid]) && $einvid != $valid_einv_uids[$einvid] ? $valid_einv_uids[$einvid] : null);
$trans_data->invoice_mark = (isset($valid_einv_marks[$einvid]) && $einvid != $valid_einv_marks[$einvid] ? $valid_einv_marks[$einvid] : null);
$trans_data->invoice_qrcode = (isset($valid_qrcode_urls[$einvid]) && $einvid != $valid_qrcode_urls[$einvid] ? $valid_qrcode_urls[$einvid] : null);
$trans_data->qrcode_img = null;
$trans_data->trans_dtime = date('Y-m-d H:i:s');
if ($trans_data->invoice_qrcode) {
/**
* Attempt to generate the QR Code image file for the current invoice correctly transmitted.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
$qrcode_fname = $this->generateInvoiceQRCode($einvid, $einv_bid, $trans_data);
if ($qrcode_fname) {
$trans_data->qrcode_img = $qrcode_fname;
}
}
// build e-invoice object for update (with "transmission data")
$data = new stdClass;
$data->id = $einvid;
$data->transmitted = 1;
$data->trans_data = json_encode($trans_data);
// update e-invoice record
$this->updateEInvoice($data);
// check if the PDF invoice requires a refresh to let the conditional text rules run after having updated the e-invoice
if ($qrcode_fname) {
// attempt to refresh the PDF invoice, if available, in case it uses the Conditional Text Rules
if ($this->refreshPdfInvoice($einv_bid)) {
$qrcode_url = VikBookingMydataAadeConstants::getQRCodeBase('uri', $qrcode_fname);
// trigger an event to allow third-party plugins to do something, like sending the invoice via email
VBOFactory::getPlatform()->getDispatcher()->trigger('onMydataAfterQrcodeInvoiceSubmitted', [$einv_bid, $einvid, $trans_data, $qrcode_url]);
}
}
/**
* Check if an e-invoice for the environmental fee was prepared to be generated and transmitted as well.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
$env_fee_inv_details = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einvid, $einv_bid), []);
if ($env_fee_inv_details && $trans_data->invoice_mark) {
/**
* It is recommended to sleep at least one second after the main invoice has been
* transmitted and before the correlated invoice gets transmitted to ensure the
* main invoice mark is registered on the myDATA platform.
*/
sleep(1);
// use the obtained invoice mark to adjust and transmit the environmental fee invoice
$this->transmitCorrelatedInvoice(
$trans_data,
$env_fee_inv_details,
[
'einvid' => $einvid,
'bid' => $einv_bid,
]
);
}
}
// display info message
$this->setInfo('Electronic invoices transmitted: ' . count($einvspool));
// we need to unset the bookings var so that the later call to getBookingsData() made by the View will reload the information
$this->bookings = [];
// unset also cols, rows and footer row to not merge data
$this->cols = [];
$this->rows = [];
$this->footerRow = [];
return true;
}
/**
* Generates one XML string for the request body to myDATA. If more
* than one e-invoice ID passed, attempts to parse all XML files for
* the already generated e-invoices in order to compose one single
* XML request body that contains all invoices. Every e-invoice has
* got an XML file compliant for the transmission, but when we need
* to transmit in mass multiple e-invoices, we try to use just one
* HTTP request by merging all e-invoices into one single XML file.
*
* @param array $einvspool an array of e-invoice IDs
* @param array $settings the driver settings
*
* @return bool|string false on failure or XML request body string.
*/
protected function buildTransmissionXMLBody($einvspool, $settings)
{
if (!is_array($einvspool) || !$einvspool) {
return false;
}
// the list of XML strings
$xml_strings = [];
// generate XML files for the requested e-invoice IDs
foreach ($einvspool as $einvid) {
// load e-invoice details
$einv_data = $this->loadEInvoiceDetails($einvid);
if (!$einv_data || !is_array($einv_data) || empty($einv_data['xml'])) {
// all e-invoices must exist as they will be set to transmitted=1 so we break the process
$this->setError('Unable to load data for the electronic invoice ID ' . $einvid);
return false;
}
// push e-invoice content
$xml_strings[] = $einv_data['xml'];
}
if (!$xml_strings) {
// no XML files created, break the process
return false;
}
if (count($xml_strings) === 1) {
// just one XML file, no need to build an XML container
return $xml_strings[0];
}
// return one whole XML request body for all e-invoices
return $this->mergeXMLInvoices($xml_strings);
}
/**
* Given a list of XML e-invoice strings, attempts to merge them
* into one single XML to avoid making one HTTP request per e-invoice.
*
* @param array $xml_strings list of XML strings for each e-invoice.
*
* @return bool|string false or whole XML string for all e-invoices.
*/
protected function mergeXMLInvoices($xml_strings)
{
if (!is_array($xml_strings) || !$xml_strings) {
return false;
}
if (count($xml_strings) === 1) {
return $xml_strings[0];
}
if (!class_exists('SimpleXMLElement')) {
/**
* We cannot afford to do a string manipulation only because SimpleXMLElement
* is missing on the server. It has to be available, it's a native library.
*/
$this->setError('SimpleXMLElement is missing on your server.');
$this->setError('This is unusual, and you should contact your hosting company to enable this native PHP library.');
$this->setError('You can only transmit single e-invoices, not more than one because SimpleXMLElement is missing');
return false;
}
/**
* Define the XML root element for the InvoicesDoc message.
* Namespace attributes will affect the incomeClassification sub nodes.
*
* @see VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
* @see https://mydata-dev.portal.azure-api.net/issues/5f3c411ac75730207831ead4
*/
$root_namespaces = VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
$xml_root = <<<XML
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<InvoicesDoc $root_namespaces>
</InvoicesDoc>
XML;
// get the SimpleXMLElement object
$xml = new SimpleXMLElement($xml_root);
// define the namespace rules for the children elements of <incomeClassification>
$child_nmspaces = [
'incomeClassification' => VikBookingMydataAadeConstants::getInvoiceChildrenNamespace()
];
// parse all e-invoices
$parsed = 0;
foreach ($xml_strings as $k => $einvoice) {
$xml_einvoice = simplexml_load_string($einvoice);
if (!is_object($xml_einvoice)) {
$this->setWarning('Unable to parse the XML of the e-invoice index ' . ($k + 1));
$this->setWarning($this->libxml_display_errors());
continue;
}
// append XML tree to a new <invoice> node
$invoice_node = $xml->addChild('invoice');
$this->simpleXmlAppendTree($invoice_node, $xml_einvoice->invoice, $child_nmspaces);
// increase parsed invoices
$parsed++;
}
if (!$parsed) {
$this->setError('No e-invoices could be parsed to merge the XML trees and related nodes into one single XML body.');
return false;
}
// get the whole XML request body just built from all e-invoices
$full_xml = $xml->asXML();
/**
* When appending child nodes with namespaces to "<incomeClassification>", these may be added as
* "<N1:classificationCategory xmlns:N1="N1">category1_3</N1:classificationCategory>" so with both
* the proper namespace in the node name, but also with the attribute 'xmlns:N1="N1"' which is making
* the whole XML failing according to the schema. Therefore, we manipulate the string to remove such attributes.
*/
if (!empty($child_nmspaces['incomeClassification'])) {
$seek_pattern = $child_nmspaces['incomeClassification'];
$full_xml = str_replace('xmlns:' . $seek_pattern . '="' . $seek_pattern . '"', '', $full_xml);
}
// attempt to properly format the XML string
$this->formatXmlString($full_xml);
// return the whole XML request body containing all the e-invoices
return $full_xml;
}
/**
* Recursive method to append a SimpleXMLElement tree node, and
* related children nodes, to another SimpleXMLElement. Used to
* dinamically add an entire tree of a single e-invoice XML file
* under a single node <invoice> of the whole XML request body.
*
* @param SimpleXMLElement $xml_to the node where the tree will be appended.
* @param SimpleXMLElement $xml_from the element to append with all its children.
* @param array $child_nmspaces associative list of children namespaces.
*
* @return void
*/
protected function simpleXmlAppendTree(&$xml_to, &$xml_from, $child_nmspaces = [])
{
$child_nmspace = null;
$child_isprefix = false;
$node_name = $xml_to->getName();
if (!empty($child_nmspaces[$node_name])) {
$child_nmspace = $child_nmspaces[$node_name];
$child_isprefix = true;
}
foreach ($xml_from->children($child_nmspace, $child_isprefix) as $xml_child) {
$add_node_name = $xml_child->getName();
if (!empty($child_nmspace)) {
$add_node_name = "$child_nmspace:$add_node_name";
}
$xml_temp = $xml_to->addChild($add_node_name, (string)$xml_child, $child_nmspace);
foreach ($xml_child->attributes() as $attr_key => $attr_value) {
$xml_temp->addAttribute($attr_key, $attr_value);
}
$this->simpleXmlAppendTree($xml_temp, $xml_child, $child_nmspaces);
}
}
/**
* Loads the details of the given e-invoice ID. The given ID should not be obliterated.
*
* @param int $einvid the ID of the e-invoice
*
* @return mixed array if the e-invoice exists and is not obliterated, false otherwise.
*/
protected function loadEInvoiceDetails($einvid)
{
if (empty($einvid)) {
return false;
}
$q = "SELECT * FROM `#__vikbooking_einvoicing_data` WHERE `id`=" . (int)$einvid . " AND `obliterated`=0;";
$this->dbo->setQuery($q);
$einv = $this->dbo->loadAssoc();
return $einv ? $einv : false;
}
/**
* Performs a POST request to the myDATA infrastructure.
*
* @param string $url_path the path to append to the base endpoint URI.
* @param mixed $body the request body.
* @param array $settings driver settings or any other option to inject.
*
* @return JHttpResponse object with code and body properties
*/
protected function myDATARequestPOST($url_path = '', $body = null, $settings = [])
{
if (empty($settings)) {
$settings = $this->loadSettings();
}
$aade_user_id = $settings['params']['aade_user_id'];
$aade_subscription_key = $settings['params']['aade_subscription_key'];
$aade_endp_url = $settings['params']['mydata_endpoint_url'];
if (!empty($settings['params']['test_mode'])) {
$aade_endp_url = VikBookingMydataAadeConstants::getDevEndpointBaseUrl();
}
if (!empty($url_path)) {
$aade_endp_url .= ltrim($url_path, '/');
}
// build request headers
$headers = [
'Content-Type' => 'application/xml',
'aade-user-id' => $aade_user_id,
'Ocp-Apim-Subscription-Key' => $aade_subscription_key,
];
// invoke CMS native transporter
$transporter = new JHttp;
$response = $transporter->post($aade_endp_url, $body, $headers);
if ($response->code != 200) {
$this->setError('Erroneous response with HTTP code ' . $response->code);
$this->setError(htmlentities($response->body));
}
return $response;
}
/**
* Checks if the XML response string from myDATA contains errors.
* Returns an array with boolean "success" and array with "einv_id => einv_mark".
*
* @param string $body the raw response body from the request.
* @param array $einvspool list of e-invoice ids in VBO just transmitted.
* @param array $einvnumbs list of e-invoice numbers in VBO just transmitted.
*
* @return array to be used with list($success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls).
*/
protected function myDATAParseXMLResponse($body, $einvspool = [], $einvnumbs = [])
{
// the default information to return
$success = false;
$valid_einv_marks = [];
$valid_einv_uids = [];
$valid_qrcode_urls = [];
$res_obj = $body;
if (!is_object($res_obj)) {
$res_obj = simplexml_load_string($body);
}
if (!is_object($res_obj) || !isset($res_obj->response)) {
$this->setError('Could not parse XML response');
$this->setError('<pre>' . htmlentities($body) . '</pre>');
return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
}
// errors counter
$errors_found = 0;
// loop through each response node
foreach ($res_obj->response as $invoice_resp) {
if (!isset($invoice_resp->statusCode)) {
$this->setError('Unexpected nodes in XML response (missing statusCode)');
$this->setError('<pre>' . htmlentities($body) . '</pre>');
return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
}
/**
* Endpoint POST /SendInvoices only (/CancelInvoice would not return this data).
* Get the index of the current invoice response (line-number starts from 1).
*/
$invoice_index = isset($invoice_resp->index) ? (int)$invoice_resp->index : 0;
// check if we have a successful status code for this invoice
if (!strcasecmp((string)$invoice_resp->statusCode, 'Success')) {
// success!
if ($invoice_index > 0 && isset($einvspool[($invoice_index - 1)])) {
// push successful invoice
$einv_id = $einvspool[($invoice_index - 1)];
// get the invoice UID
$invoice_uid = isset($invoice_resp->invoiceUid) ? (string)$invoice_resp->invoiceUid : $einv_id;
$valid_einv_uids[$einv_id] = $invoice_uid;
// get the invoice mark (needed for a later cancellation)
$invoice_mark = isset($invoice_resp->invoiceMark) ? (string)$invoice_resp->invoiceMark : $einv_id;
$valid_einv_marks[$einv_id] = $invoice_mark;
/**
* Check if a QRCode URL is available for the electronic invoice.
* Valid property name should be "qrUrl".
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
$invoice_qrcode = isset($invoice_resp->qrUrl) ? (string)$invoice_resp->qrUrl : $einv_id;
$invoice_qrcode = !isset($invoice_resp->qrUrl) && isset($invoice_resp->qrCodeUrl) ? (string)$invoice_resp->qrCodeUrl : $invoice_qrcode;
$valid_qrcode_urls[$einv_id] = $invoice_qrcode;
}
continue;
}
// at this point we expect an error
if (!isset($invoice_resp->errors) || !isset($invoice_resp->errors->error)) {
// errors should be set, but if they aren't, this is unexpected
$this->setError('Unexpected nodes in XML response (missing errors or error)');
$this->setError('<pre>' . htmlentities($body) . '</pre>');
return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
}
// loop through the errors
foreach ($invoice_resp->errors->error as $resp_err) {
$errors_found++;
$err_code = isset($resp_err->code) ? (string)$resp_err->code : '0';
$err_mess = isset($resp_err->message) ? (string)$resp_err->message : '???';
$inv_numb = isset($einvnumbs[($invoice_index - 1)]) ? $einvnumbs[($invoice_index - 1)] : '???';
$this->setError(sprintf('Error (%s) in invoice index %d (#%s): %s', $err_code, $invoice_index, $inv_numb, $err_mess));
}
}
// if we had no errors at all, the response was successful
$success = (!$errors_found);
return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
}
/**
* Downloads the electronic invoices by storing temporary files.
* This is a 'driver action', and so it's called before getBookingsData()
* in the view. This method will not save/update records in the DB.
*
* @return void
*/
public function downloadEInvoices()
{
// make sure the transmission settings are not empty
$settings = $this->loadSettings();
if ($settings === false || !$settings['params']) {
$this->setError('Missing settings. Please set up the driver first.');
return false;
}
// call the main method to generate rows, cols and bookings array
$this->getBookingsData();
if (strlen($this->getError()) || !$this->bookings) {
return false;
}
// pool of e-invoice IDs to download
$einvspool = [];
foreach ($this->bookings as $gbook) {
// make sure an electronic invoice was already issued for this booking ID by this driver
if (empty($gbook[0]['einvid']) || $gbook[0]['einvdriver'] != $this->getDriverId()) {
// no e-invoices available for this booking, skipping
continue;
}
// push e-invoice ID to the pool
array_push($einvspool, $gbook[0]['einvid']);
}
if (!$einvspool) {
// no e-invoices generated
$this->setWarning('No electronic invoices can be downloaded. Please generate them first.');
return false;
}
// build one whole XML file
$einv_xml_body = $this->buildTransmissionXMLBody($einvspool, $settings);
if ($einv_xml_body === false) {
// something went wrong with the creation of the file to download
$this->setError('Could not generate the XML file containing all the electronic invoices.');
return false;
}
// force the download of the XML string
header('Content-Disposition: attachment; filename="mydata-aade-einvoices' . date('Y-m-d') . '.xml"');
header("Content-Type: text/xml");
header("Content-Length:" . strlen($einv_xml_body));
header('Connection: close');
echo $einv_xml_body;
exit;
}
/**
* Forces the display of an electronic invoice. This is a 'driver action', and so it's called
* before getBookingsData() in the view. This method will not save/update records in the DB.
* This method truncates the execution of the script to read the XML data.
*
* @return void
*/
public function viewEInvoice()
{
$einvid = VikRequest::getInt('einvid', 0, 'request');
$einv_data = $this->loadEInvoiceDetails($einvid);
if (!$einv_data) {
die('Missing e-invoice ID');
}
// force the output
header("Content-type:text/xml");
echo $einv_data['xml'];
exit;
}
/**
* Removes an electonic invoice. This is a 'driver action',
* and so it's called before getBookingsData() in the view.
* It also removes the analogic version in PDF of the invoice.
*
* @return void
*/
public function removeEInvoice()
{
$einvid = VikRequest::getInt('einvid', '', 'request');
$einv_data = $this->loadEInvoiceDetails($einvid);
if (!$einv_data) {
$this->setError('Missing e-invoice ID. Unable to delete the e-invoice.');
return false;
}
// get "transmission data" (if any)
$trans_data = !empty($einv_data['trans_data']) ? json_decode($einv_data['trans_data']) : null;
if (is_object($trans_data) && !empty($trans_data->invoice_mark)) {
/**
* This invoice was transmitted before, make sure to cancel it also from myDATA.
* However, the endpoint requires a "mark" value for the invoice, which could be the
* invoiceMark property upon a successful submission or the number we pass to compose
* the XML of the electronic invoice (our progressive number). There are two "mark"
* values, but we got errors for both, hence we don't know which one to use. We always
* check if $trans_data->invoice_mark is not empty so that we know the invoice was
* already transmitted before to myDATA and accepted.
*
* @todo what's the right invoice mark? the "number" is inside the XML that we generate
* even before the transmission, while "invoice_mark" is returned in the myDATA response.
*/
$mydata_invoice_mark = $einv_data['number'];
$mydata_invoice_mark = $trans_data->invoice_mark;
/**
* Check if a correlated invoice was transmitted, because it should be removed first.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
$correlated_inv_data_tn = $this->getPreviousCorrelatedInvoiceData($einv_data['id'], $einv_data['idorder'], 'transmission');
if ($correlated_inv_data_tn && !empty($correlated_inv_data_tn['mark'])) {
// delete the correlated invoice first, by making the POST request
$response = $this->myDATARequestPOST('CancelInvoice?mark=' . $correlated_inv_data_tn['mark']);
if ($response->code != 200) {
// the request was not successful
$this->setWarning(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
$this->setWarning('Could not cancel the correlated invoice from myDATA with mark ' . $correlated_inv_data_tn['mark']);
} else {
// check the XML response
$xml_result = $this->myDATAParseXMLResponse($response->body);
if ($xml_result[0]) {
// success! the correlated invoice was cancelled from myDATA
$this->setInfo(sprintf('Correlated invoice mark %s successfully cancelled from myDATA', $correlated_inv_data_tn['mark']));
/**
* IMPORTANT: sleep for 2 seconds, or deleting immediately the main invoice below may
* result into an error like "Error (255) in invoice index 0 (#???): Invoice with MARK 400001924172498
* cannot be cancelled because it is connected with active invoice with MARK 400001924172499"
*/
sleep(2);
}
}
// always delete the record for the correlated invoice
VBOFactory::getConfig()->remove($this->getCorrelatedInvoiceParamName($einv_data['id'], $einv_data['idorder']));
// check if the PDF version of the environmental fee exists
if (!empty($correlated_inv_data_tn['pdf'])) {
$envfee_pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $correlated_inv_data_tn['pdf'];
if (is_file($envfee_pathpdf)) {
@unlink($envfee_pathpdf);
}
}
}
// make the POST request
$response = $this->myDATARequestPOST('CancelInvoice?mark=' . $mydata_invoice_mark);
if ($response->code != 200) {
// the request was not successful, and the XML invoices were not parsed at all by myDATA
$this->setWarning(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
$this->setWarning('Could not cancel the invoice from myDATA.');
} else {
// check the XML response
$xml_result = $this->myDATAParseXMLResponse($response->body);
if ($xml_result[0]) {
// success! the invoice was cancelled from myDATA
$this->setInfo(sprintf('Invoice mark %s (#%s) successfully cancelled from myDATA', $trans_data->invoice_mark, $einv_data['number']));
}
}
}
// remove e-invoice
$q = "DELETE FROM `#__vikbooking_einvoicing_data` WHERE `id`=".$einv_data['id'].";";
$this->dbo->setQuery($q);
$this->dbo->execute();
// remove analogic invoice for this booking
$pdfremoved = false;
if (!empty($einv_data['idorder'])) {
$pdfname = '';
$q = "SELECT * FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$einv_data['idorder'].";";
$this->dbo->setQuery($q);
$analogic = $this->dbo->loadAssoc();
if ($analogic) {
$pdfname = $analogic['file_name'];
$q = "DELETE FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$einv_data['idorder'].";";
$this->dbo->setQuery($q);
$this->dbo->execute();
}
$pdfpath = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . $pdfname;
if (!empty($pdfname) && is_file($pdfpath)) {
$pdfremoved = true;
@unlink($pdfpath);
}
}
$this->setInfo(($pdfremoved ? 'Electronic and PDF invoices deleted' : 'Electronic invoice deleted'));
}
/**
* Attempts to generate a QR Code PNG image file with the e-invoice URL.
*
* @param int $einv_id the generated e-invoice record ID.
* @param int $bid the reservation record ID.
* @param object $data transaction data object with myDATA values.
*
* @return string empty string in case of failure, or generated QR Code file name.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
protected function generateInvoiceQRCode($einv_id, $bid, $data)
{
if (!is_object($data) || empty($data->invoice_qrcode)) {
return '';
}
// the QR Code PNG file name
$filename = "aade_qrcode_{$bid}_{$einv_id}.png";
// generate the image
if ($this->generateQRCodeImage($data->invoice_qrcode, VikBookingMydataAadeConstants::getQRCodeBase('path', $filename))) {
// file was written successfully
return $filename;
}
// an error has occurred
return '';
}
/**
* Generates a QR Code image with the given URL in the given path.
*
* @param string $url the URL to be assigned (content) to the QR Code.
* @param string $path the full path where the file should be saved.
*
* @return bool
*/
protected function generateQRCodeImage($url, $path)
{
try {
// require the TCPDF 2D Barcode library
require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf_barcodes_2d.php';
// set the barcode content and type
$barCode = new TCPDF2DBarcode($url, 'QRCODE,H');
// generate the QR code as PNG image
$qr = $barCode->getBarcodePngData(
VikBookingMydataAadeConstants::QRCODE_PNG_WIDTH,
VikBookingMydataAadeConstants::QRCODE_PNG_HEIGHT,
explode(',', preg_replace("/[^0-9\.\,]/", '', VikBookingMydataAadeConstants::QRCODE_PNG_COLOR_RGB))
);
// write the image on disk
return (bool)JFile::write($path, $qr);
} catch (Throwable $t) {
// do nothing
}
return false;
}
/**
* Right after obtaining a QR Code for an electronic invoice, the driver
* calls this method to refresh the PDF (courtesy) invoice so that any
* conditional text rule used on the invoice template file will run correctly.
*
* @param int $bid the reservation record ID for which the invoice should be refreshed.
*
* @return bool
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
protected function refreshPdfInvoice($bid)
{
return (bool)VikBooking::generateBookingInvoice(
// load booking details
VikBooking::getBookingInfoFromID($bid),
// do not set an invoice number, because the previous one must be used
$invoice_num = 0,
// do not set an invoice suffix, because the previous one must be used
$invoice_suff = '',
// do not set an invoice date, because the previous one must be used
$invoice_date = '',
// company information will be re-fetched
$company_info = '',
// translation is not needed
$translate = false,
// set the argument to request a re-generation (refresh) of the existing invoice
$refresh_pdf = true
);
}
/**
* Validates the XML against the Schema.
*
* @param string $xml the xml string to validate
*
* @return null|boolean
*/
protected function validateXmlAgainstSchema($xml) {
if (!class_exists('DOMDocument')) {
// we cannot validate the XML because DOMDocument is missing
return null;
}
$schema_path = VikBookingMydataAadeConstants::getSchemaPath();
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->load($xml);
if (!$dom->schemaValidate($schema_path)) {
$this->setWarning('The schema validation of the electronic XML invoice returned errors, but they may be related to an unreadable schema.');
$this->setWarning($this->libxml_display_errors());
return false;
}
return true;
}
/**
* Formats the XML errors occurred
*
* @return string the error string
*/
protected function libxml_display_errors() {
$errorstr = "";
$errors = libxml_get_errors();
foreach ($errors as $error) {
$errorstr .= $this->libxml_display_error($error);
}
libxml_clear_errors();
return $errorstr;
}
/**
* Explanation of the XML error
*
* @param object $error the libxml error object
*
* @return string the explained error occurred
*/
protected function libxml_display_error($error) {
$return = "\n";
switch ($error->level) {
case LIBXML_ERR_WARNING :
$return .= "Warning ".$error->code.": ";
break;
case LIBXML_ERR_ERROR :
$return .= "Error ".$error->code.": ";
break;
case LIBXML_ERR_FATAL :
$return .= "Fatal Error ".$error->code.": ";
break;
}
$return .= trim($error->message);
if ($error->file) {
$return .= " in " . $error->file;
}
$return .= " on line " . $error->line . "\n";
return $return;
}
/**
* Override method to show the overlay content.
* Used to display the edit form of the raw XML.
* This method echoes the string to be displayed.
*
* @return void
*/
public function printOverlayContent()
{
$content = VikRequest::getString('drivercontent', '', 'request');
$einvid = VikRequest::getInt('einvid', 0, 'request');
$envfeebid = VikRequest::getInt('envfeebid', 0, 'request');
if ($content == 'editEInvoice' && !empty($einvid)) {
$einv_data = $this->loadEInvoiceDetails($einvid);
if (!$einv_data) {
return;
}
if ($envfeebid) {
$correlated_invoice = $this->getPreviousCorrelatedInvoiceData($einvid, $envfeebid, $type = 'record');
if ($correlated_invoice) {
$einv_data['correlated_invoice'] = $correlated_invoice;
}
}
// path to edit invoice layout file
$fpath = $this->driverHelperPath . 'editeinvoice.php';
// load helper file and echo its content
echo $this->loadHelperFile($fpath, $einv_data);
return;
}
}
/**
* Updates the XML of an electonic invoice. This is a 'driver action',
* and so it's called before getBookingsData() in the view.
*
* @return bool
*/
public function updateXmlEInvoice()
{
$einvid = VikRequest::getInt('einvid', '', 'request');
$newxml = VikRequest::getString('newxml', '', 'request', VIKREQUEST_ALLOWRAW);
$einv_data = $this->loadEInvoiceDetails($einvid);
if (!$einv_data) {
$this->setError('Invoice not found');
return false;
}
if (empty($newxml)) {
$this->setError('Empty XML content');
return false;
}
$jdate = new JDate;
$data = new stdClass;
$data->id = $einv_data['id'];
$data->created_on = $jdate->toSql();
$data->xml = $newxml;
return $this->updateEInvoice($data);
}
/**
* Updates the XML of a correlated electonic invoice. This is a 'driver action',
* and so it's called before getBookingsData() in the view.
*
* @return bool
*
* @since 1.16.7 (J) - 1.6.7 (WP)
*/
public function updateCorrelatedXmlEInvoice()
{
$einvid = VikRequest::getInt('einvid', 0, 'request');
$envfeebid = VikRequest::getInt('envfeebid', 0, 'request');
$newxml = VikRequest::getString('newxml', '', 'request', VIKREQUEST_ALLOWRAW);
// get the invoice record
$correlated_inv_raw_data = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einvid, $envfeebid), []);
if (!$correlated_inv_raw_data) {
return false;
}
// update the XML source code
$correlated_inv_raw_data['xml'] = $newxml;
// update record
VBOFactory::getConfig()->set($this->getCorrelatedInvoiceParamName($einvid, $envfeebid), $correlated_inv_raw_data);
return true;
}
/**
* Extracts only numbers from a given string, by optionally
* stripping the current year. Useful to find an invoice number.
*
* @param string $str the string to look for numbers
* @param boolean $stripy whether to strip the current year
*
* @return string either an empty string or all numbers as a concatenated string
*/
protected function getOnlyNumbers($str, $stripy = false)
{
if ($stripy) {
$str = str_replace(date('Y'), '', $str);
}
preg_match_all('/\d+/', $str, $matches);
return implode('', $matches[0]);
}
}