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' => '', 'html' => '', 'type' => 'calendar', 'name' => 'fromdate' ); array_push($this->driverFilters, $filter_opt); // To Date Filter $filter_opt = array( 'label' => '', 'html' => '', '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 propresendecofee = "resendecofeebid"+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; pobj[propresendecofee] = 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; pobj[propresendecofee] = 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; pobj[propresendecofee] = 0; } else if (splitval[1] == "resendecofee") { // update cell data attribute for CSS to transmitted jQuery(this).closest("td").attr("data-einvaction", -1); pobj[propregen] = 0; pobj[propresend] = 0; pobj[propresendecofee] = 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 .= ''; } $filter_opt = array( 'label' => '', 'html' => '', 'type' => 'select', 'name' => 'monyear' ); array_push($this->driverFilters, $filter_opt); } } // date type filter $filter_opt = array( 'label' => '', 'html' => '', 'type' => 'select', 'name' => 'datetype' ); array_push($this->driverFilters, $filter_opt); // invoice type filter $filter_opt = array( 'label' => '', 'html' => '', 'type' => 'select', 'name' => 'einvtype' ); array_push($this->driverFilters, $filter_opt); // search invoice filter $filter_opt = array( 'label' => '', 'html' => '
', '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, ' '.JText::translate('VBODRIVERGENERATEINVS').' '); // transmit invoices button array_push($this->driverButtons, ' Transmit to myDATA '); // download invoices button array_push($this->driverButtons, ' Download XML files '); 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`,`ei`.`trans_data` AS `einvtndata` ". "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`.`trans_data` AS `einvtndata`,`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`,`ei`.`trans_data` AS `einvtndata` ". "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`.`trans_data` AS `einvtndata`,`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) && $customer && !empty($customer['country'])) { $country3 = $customer['country']; $gbook[0]['country'] = $country3; } if (empty($country2) && $customer && !empty($customer['country_2_code'])) { $country2 = $customer['country_2_code']; $gbook[0]['country_2_code'] = $country2; } if (empty($countryfull) && $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; // attempt to decode the transaction data, if any if (!empty($gbook[0]['einvtndata']) && is_scalar($gbook[0]['einvtndata'])) { $gbook[0]['einvtndata'] = json_decode($gbook[0]['einvtndata'], true); } // set e-invoice transaction data, if any $einvtndata = is_array(($gbook[0]['einvtndata'] ?? null)) ? $gbook[0]['einvtndata'] : null; // 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 ' '.JText::translate('VBOMANUALINVOICE').''; } /** * 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, ' '.JText::translate('VBOMANUALINVOICE').' (' . $analog_info['for_date'] . ')'); } return implode('
';
}
}
} else {
// if empty customer ($val) print danger button to assign a customer to this booking ID
$cont = '' . JText::translate('VBOCREATENEWCUST') . '';
}
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 = '' . JText::translate('VBCONFIGCLOSINGDATEADD') . '';
} else {
$cont = '-----';
}
return $cont;
}
if (count($customer) && empty($customer['zip'])) {
// postal code is mandatory
return 'No Postal Code';
}
if (count($customer) && empty($customer['address'])) {
// address is mandatory
return 'No Address';
}
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 may be mandatory for both issuer and counterpart
$cont = '' . JText::translate('VBCONFIGCLOSINGDATEADD') . '';
} else {
// if empty customer ($val) print danger button to assign a customer to this booking ID
$cont = '' . JText::translate('VBCONFIGCLOSINGDATEADD') . '';
}
}
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 = ''.$cont.'';
}
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, '');
array_push($buttons, '');
$correlated_inv_numb = $this->getPreviousCorrelatedInvoiceData($noinvoicereason, $bid);
if ($correlated_inv_numb) {
array_push($buttons, '');
}
array_push($buttons, '');
}
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, $einvtndata) {
if ($val === 0) {
// invoice cannot be issued
$noinvoicereason = empty($noinvoicereason) ? 'Missing data to generate the invoice' : $noinvoicereason;
return '';
}
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 '';
}
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 '';
}
// invoice can be issued: print drop down to let the customer skip this generation
return '';
},
'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' => ''.htmlentities($xml).'
' . htmlentities($response->body) . ''); return false; } if (!isset($res_obj->response->statusCode)) { $this->setError('Unexpected nodes in XML response (missing statusCode)'); $this->setError('
' . htmlentities($response->body) . ''); 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; /** * Update the original XML on main invoice to set the content of the nodes UID and Mark, because * they were removed at runtime before the transmission after the myDATA update (Jan 2026). * * @since 1.18.6 (J) - 1.8.6 (WP) */ if ($invoice_uid && $invoice_mark) { // load XML e-invoice $dom = new DOMDocument(); $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $dom->loadXML($correlated_einv_final_xml); // locate the parent
' . htmlentities($response->body) . ''); 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 = []; // list of eco-fee-only invoices to be (re-)transmitted $ecofee_einvs_retn = []; 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'); $resendecofee = VikRequest::getInt('resendecofeebid'.$gbook[0]['id'], 0, 'request'); if ($resendecofee) { // push record for the eco-fee invoice re-transmit only $ecofee_einvs_retn[] = $gbook[0]; continue; } if (!$resend) { // 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 && $ecofee_einvs_retn) { // pre-check: conflict with re-transmission of eco-fee invoice(s) and main invoice first transmission or re-transmission $this->setWarning('If you choose to re-transmit just eco-fee invoices, then any other main invoice should be excluded from the transmission to avoid conflicts.'); return false; } if (!$ecofee_einvs_retn) { // attempt to transmit or re-transmit the main invoices with their (eventually) related eco-fee invoices 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('
' . htmlentities($einv_xml_body) . ''); } // 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('
' . htmlentities($response->body) . ''); } // 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; } } /** * Update the original XML on main invoice to change the content of the nodes UID and Mark, even * if they will be removed at runtime before the transmission after the myDATA update (Jan 2026). * * @since 1.18.6 (J) - 1.8.6 (WP) */ $updatedEinvoiceXML = null; $prev_einv_data = $this->loadEInvoiceDetails($einvid); if ($trans_data->invoice_uid && $trans_data->invoice_mark && !empty($prev_einv_data['xml'])) { // load XML e-invoice $dom = new DOMDocument(); $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $dom->loadXML($prev_einv_data['xml']); // locate the parent
' . htmlentities($body) . ''); 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('
' . htmlentities($body) . ''); 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('
' . htmlentities($body) . ''); 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]); } }