* Copyright (C) 2023 Deák Ferenc * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ use Luracast\Restler\RestException; // dol_include_once('/booking/class/bookinglog.class.php'); require_once DOL_DOCUMENT_ROOT . '/custom/bbus/class/api_bbus.class.php'; require_once DOL_DOCUMENT_ROOT . '/custom/bbus/class/api_bbus_log.class.php'; require_once DOL_DOCUMENT_ROOT . '/custom/bbus/class/apiinvoicehelper.class.php'; require_once DOL_DOCUMENT_ROOT . '/custom/bbus/class/apiproductlisthelper.class.php'; require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php'; require_once DOL_DOCUMENT_ROOT . '/custom/bbus/class/api_curl.class.php'; require_once DOL_DOCUMENT_ROOT . '/custom/bbus/class/basicservices.class.php'; /** * \file booking/class/api_booking.class.php * \ingroup booking * \brief File for API management of bookinglog. */ /** * API class for booking bookinglog * * @class DolibarrApiAccess {@requires user,external} */ class BookingApi extends DolibarrApi { use CurlApi; /** * @var BookingLog $bookinglog {@type BookingLog} */ public $bookinglog; private User $user; /** * Constructor * * @url GET / * */ public function __construct() { global $db, $user; $this->db = $db; $this->user = $user; // $this->bookinglog = new BookingLog($this->db); } //P4NR6zhWHage //!!!types: /** * Get types of a bookable events * * Return an array with bookable events type list * * @return array|mixed data without useless information * * @url GET types * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function types() { dol_include_once('/eventwizard/class/eventdetails.class.php'); $EventDetails = new EventDetails($this->db); return $EventDetails->fields['type']['arrayofkeyval']; if (!DolibarrApiAccess::$user->rights->booking->types->read) { throw new RestException(401); } // dol_include_once('/booking/class/bookinglog.class.php'); // $result = $this->bookinglog->fetch($id); // if (!$result) { // throw new RestException(404, 'BookingLog not found'); // } // if (!DolibarrApi::_checkAccessToResource('bookinglog', $this->bookinglog->id, 'booking_bookinglog')) { // throw new RestException(401, 'Access to instance id='.$this->bookinglog->id.' of object not allowed for login '.DolibarrApiAccess::$user->login); // } // return $this->_cleanObjectDatas($this->bookinglog); } /** * Get bookable events * * Return an array with bookable events list * * @return array|mixed data without useless information * * @param int $onlyBookable * @param int $withDetails * @param int $withDates * @param int $withAviability * @url GET events * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function events($onlyBookable = 1, $withDetails = 0, $withDates = 0, $withAviability = 0) { global $db, $conf; // if (!DolibarrApiAccess::$user->rights->booking->events->read) { // throw new RestException(401); // } $obj_ret = []; // if (!DolibarrApiAccess::$user->rights->agenda->myactions->read) { // throw new RestException(401, "Insufficient rights to read events"); // } $sql = "SELECT DISTINCT ed.rowid as id"; if (!empty($conf->societe->enabled)) { // if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) { // $sql .= ", sc.fk_soc, sc.fk_user"; // We need these fields in order to filter by sale (including the case where the user can only see his prospects) // } } $sql .= " ,ed.label "; $sql .= " ,eld.label as from"; //... ?? $sql .= " ,ela.label as to"; //... $sql .= " FROM " . MAIN_DB_PREFIX . "actioncomm as t"; $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "eventwizard_eventdetails ed ON ed.rowid = t.fk_element"; $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "eventwizard_eventlocation eld ON eld.rowid = ed.fk_elventlocation_departure"; $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "eventwizard_eventlocation ela ON ela.rowid = ed.fk_elventlocation_arrival"; $sql .= " WHERE code = 'AC_EVENT' "; $sql .= " AND t.elementtype = 'eventdetails@eventwizard'"; if ($onlyBookable) { $sql .= " AND t.datep > CURRENT_TIMESTAMP"; } $result = $this->db->query($sql); if ($result) { $i = 0; // $num = $this->db->num_rows($result); // $min = min($num, ($limit <= 0 ? $num : $limit)); while ($event = $this->db->fetch_object($result)) { if ($withDetails) { $event->details = $this->details($event->id); } if ($withDates) { //... $withAviability $event->dates = $this->dates($event->id); } $obj_ret[] = $event; } } else { throw new RestException(503, 'Error when retrieve Agenda Event list : ' . $this->db->lasterror()); } if (!count($obj_ret)) { throw new RestException(404, 'No Agenda Event found'); } return $obj_ret; } /** * Get event dates * * Return an array with event dates list * * @return array|mixed data without useless information * * @param int $id * @param int $onlyBookable * @param string $json * @url GET dates * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function dates($id, $onlyBookable = 1, $json = null) { global $db, $conf; // if (!DolibarrApiAccess::$user->rights->booking->events->read) { // throw new RestException(401); // } $obj_ret = []; // if (!DolibarrApiAccess::$user->rights->agenda->myactions->read) { // throw new RestException(401, "Insufficient rights to read events"); // } $sql = "SELECT t.id "; if (!empty($conf->societe->enabled)) { // if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) { // $sql .= ", sc.fk_soc, sc.fk_user"; // We need these fields in order to filter by sale (including the case where the user can only see his prospects) // } } $sql .= " ,ed.label, t.note "; $sql .= " ,t.datep as start, t.datep2 as end"; $sql .= " ,eld.label as from"; //... ?? $sql .= " ,ela.label as to"; //... $sql .= " FROM " . MAIN_DB_PREFIX . "actioncomm as t"; // if (!empty($conf->societe->enabled)) { // if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) { // $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; // We need this table joined to the select in order to filter by sale // } // } $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "eventwizard_eventdetails ed ON ed.rowid = t.fk_element"; $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "eventwizard_eventlocation eld ON eld.rowid = ed.fk_elventlocation_departure"; $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "eventwizard_eventlocation ela ON ela.rowid = ed.fk_elventlocation_arrival"; // $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."eventwizard_eventoption eo ON el.rowid = ed.fk_element"; // $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."eventwizard_eventproduct eo ON el.rowid = ed.fk_element"; // $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."eventwizard_ eo ON el.rowid = ed.fk_element"; // $sql .= ' WHERE t.entity IN ('.getEntity('agenda').')'; $sql .= " WHERE code = 'AC_EVENT' "; $sql .= " AND t.fk_element = " . (int) $this->db->escape($id); $sql .= " AND t.elementtype = 'eventdetails@eventwizard'"; $sql .= " AND t.datep > CURRENT_TIMESTAMP"; // if (!empty($conf->societe->enabled)) { // if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) { // $sql .= " AND t.fk_soc = sc.fk_soc"; // } // } // if ($user_ids) { // $sql .= " AND t.fk_user_action IN (".$this->db->sanitize($user_ids).")"; // } // if ($socid > 0) { // $sql .= " AND t.fk_soc = ".((int) $socid); // } // Insert sale filter // if ($search_sale > 0) { // $sql .= " AND sc.fk_user = ".((int) $search_sale); // } // Add sql filters // if ($sqlfilters) { // $errormessage = ''; // if (!DolibarrApi::_checkFilters($sqlfilters, $errormessage)) { // throw new RestException(503, 'Error when validating parameter sqlfilters -> '.$errormessage); // } // $regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^\(\)]+)\)'; // $sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")"; // } // $sql .= $this->db->order($sortfield, $sortorder); // if ($limit) { // if ($page < 0) { // $page = 0; // } // $offset = $limit * $page; // $sql .= $this->db->plimit($limit + 1, $offset); // } $result = $this->db->query($sql); if ($result) { $i = 0; $num = $this->db->num_rows($result); // $min = min($num, ($limit <= 0 ? $num : $limit)); while ($i < $num) { $obj_ret[] = $this->db->fetch_object($result); $i++; } } else { throw new RestException(503, 'Error when retrieve Agenda Event list : ' . $this->db->lasterror()); } if (!count($obj_ret)) { throw new RestException(404, 'No Agenda Event found'); } return $obj_ret; } /** * Post booking (draft) * * Return an array with draft bookings list * * @return array|mixed data without useless information * * @param int $event * @param array $details * @url POST set * * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function set($event, $details) { if (!DolibarrApiAccess::$user->rights->booking->events->write) { throw new RestException(401); } } /** * Post booking validation * * Return an array with bookings list * * @return array|mixed data without useless information * * @param int $event * @param array $drafts * @url POST validate * * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function validate($event, $drafts) { if (!DolibarrApiAccess::$user->rights->booking->events->write) { throw new RestException(401); } } /** * Get bookings list * * Return an array with the user's bookings list * * @return array|mixed data without useless information * * @url POST bookings * * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function bookings() { if (!DolibarrApiAccess::$user->rights->booking->events->write) { throw new RestException(401); } } /** * Get cron to clean booking draftInvoices * * Return an array with cleaned list * * @return array|mixed data without useless information * * @url GET cron * * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function cron() { if (!DolibarrApiAccess::$user->rights->booking->events->write) { throw new RestException(401); } } /** * Get details of a bookable event * * Return an array with details * * @return array|mixed data without useless information * * @param int $id * @url GET details * * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function details($id) { $details = [ 'tmp' => [], 'product' => [], 'min' => 0, 'id_event' => $id, // 'variants' => [] ]; $variants = []; dol_include_once('/eventwizard/class/eventdetails.class.php'); $EventDetails = new EventDetails($this->db); if ($EventDetails->fetch($id)) { $sql = "SELECT ep.rowid ,ep.fk_eventdetails, ep.fk_product, ep.type, ep.amount, ep.qty "; $sql .= " , p.label, p.price "; //price price_ttc price_min price_min_ttc $sql .= " FROM " . MAIN_DB_PREFIX . "eventwizard_eventproduct ep "; $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "product p ON p.rowid = ep.fk_product "; // $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination ac ON ac.fk_product_parent = ep.fk_product "; // $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."eventwizard_eventoption eo ON eo.rowid = ep.fk_eventoption "; // $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."eventwizard_eventdetails ed ON ed.rowid = ep.fk_eventdetails "; $sql .= " WHERE ep.fk_eventdetails = " . (int) $this->db->escape($id); // return $sql; $result = $this->db->query($sql); if ($result) { $variants_ids = []; while ($d = $this->db->fetch_object($result)) { $variants_ids[] = $d->fk_product; // if(is_null($d->fk_product_child)){} $d->variants = []; // $d->minP = (is_null($d->amount))?(float)$d->price:(float)$d->amount; $details['tmp']['' . $d->fk_product] = $d; } $sql2 = "SELECT p.rowid, p.label, p.price "; $sql2 .= ", ac.fk_product_parent, ac.fk_product_child, ac.variation_price, ac.variation_price_percentage "; // variation_price variation_price_percentage variation_weight variation_ref_ext entity $sql2 .= "FROM " . MAIN_DB_PREFIX . "product_attribute_combination ac "; $sql2 .= " LEFT JOIN " . MAIN_DB_PREFIX . "product p ON ac.fk_product_child = p.rowid "; $sql2 .= " WHERE ac.fk_product_parent IN (" . implode(",", $variants_ids) . ") "; $result2 = $this->db->query($sql2); if ($result2) { while ($v = $this->db->fetch_object($result2)) { $d = $details['tmp']['' . $v->fk_product_parent]; $d->variants[] = $this->product2out($v); if (!is_null($d->amount)) { //... - ??? %? $d->minP = (float) $d->amount; } elseif (!isset($d->minP)) { $d->minP = $v->price; } elseif ($d->minP > $v->price) { $d->minP = $v->price; } $details['tmp']['' . $v->fk_product_parent] = $d; } } foreach ($details['tmp'] as $k => $v) { $p = $this->product2out($v); $p["minP"] = (is_null($v->amount)) ? (!isset($d->minP)) ? (float) $v->price : (float) $v->minP : (float) $v->amount; $p["variants"] = $v->variants; $details['product'][] = $p; if ($p["type"] == 1) { $details['min'] += $p["minP"]; } } unset($details['tmp']); } else { throw new RestException(503, 'Error when retrieve Agenda Event list : ' . $this->db->lasterror()); } // $EventDetails->getLinesArray(); // print_r($EventDetails->lines); return $details; //$variations // $variations = } } protected function product2out($product) { return [ "id" => $product->fk_product, "type" => $product->type, //1,2,3 "fixed_price" => $product->amount, "max" => $product->qty, "label" => $product->label, "price" => $product->price, ]; } /** * Get all products by type * * Return an array with details * * @return array|mixed data without useless information * * @param int $type_id * @param string $from_date * @param string $to_date * * @url POST getallavailableproducts * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function getAllAvailableProducts(int $type_id, string $from_date, string $to_date) { $date = new DateTime($to_date); $date->modify('+1 day'); $to_date = $date->format('Y-m-d'); $TMPArray = []; $productsArray = []; $sql = "SELECT pr.rowid from llx_actioncomm as ac LEFT JOIN llx_actioncomm_extrafields as ace ON ace.fk_object = ac.id INNER JOIN llx_eventwizard_eventdetails as ed ON ed.rowid = ac.fk_element INNER JOIN llx_eventwizard_eventproduct as ep ON ep.fk_eventdetails = ed.rowid INNER JOIN llx_product as pr ON pr.rowid = ep.fk_product where ac.datep > '{$from_date} 00:00:00' AND ac.datep2 < '{$to_date} 00:00:00' AND ac.code = 'AC_EVENT' AND ed.type = {$type_id} AND ace.max_num - COALESCE(ace.participants, 0) > 1 GROUP BY pr.rowid"; $result = $this->db->query($sql); if ($this->db->num_rows($result) > 0) { while ($row = $this->db->fetch_object($result)) { $TMPArray[] = $row->rowid; } foreach ($TMPArray as $productItem) { $ApiProductListHelper = new ApiProductListHelper(); $array = $ApiProductListHelper->list('t.ref', 'ASC', '', '', '', '', "(t.rowid:=:{$productItem})"); $productsArray[] = $array[0]; } } return $productsArray; } /** * First step of the eventhandling * * Return an array with details * * @return array|mixed data without useless information * * @param int $type_id //selected evet from llx_event * @param int $fk_event //selected evet from llx_event * @param int $reservations //numbers of reservations * @param int $product_id //Product * @param string $sendId //ID * * @url POST firsteventstep * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function firstEventStep(int $type_id, int $fk_event, int $reservations, int $product_id, string $sendId) { global $conf; ApiBbusLog::appLog("{$sendId}: firstEventStep"); if (!DolibarrApiAccess::$user->rights->facture->creer) { ApiBbusLog::appLog("{$sendId} Insufficient rights"); throw new RestException(401, 'Insufficient rights'); } $this->createLog($sendId, $fk_event, $reservations, $product_id); $server_host = $this->getServerHost($type_id); if ($server_host == $conf->global->LOCAL_SERVER_HOST) { if ($this->noLocalEmptySpaces($fk_event, $reservations)) { throw new RestException(506, 'No available spaces'); } ApiBbusLog::appLog("{$sendId}: firstEventStep: local"); $createdPreOrderOBJ = $this->localcreatedpreorderobj($sendId, $fk_event, $reservations, $product_id); } else { ApiBbusLog::appLog("{$sendId}: firstEventStep: curl"); $params = compact('fk_event', 'reservations'); $postFields = json_encode($params); $noavailableSpaces = json_decode($this->curlRunner('bookingapi/curlnoLocalEmptySpaces', $postFields, 'POST', true))->result; if ($noavailableSpaces) { ApiBbusLog::appLog("{$sendId}: firstEventStep: No available spaces"); throw new RestException(506, 'No available spaces'); } $params = compact('sendId', 'fk_event', 'reservations', 'product_id'); $postFields = json_encode($params); $createdPreOrderOBJ = $this->curlRunner('bookingapi/localcreatedpreorderobj', $postFields, 'POST', true); } ApiBbusLog::appLog("{$sendId}: " . json_encode($createdPreOrderOBJ)); return $createdPreOrderOBJ; } /** * Get all preorders * * Return an array with details * * @return array|mixed data without useless information * * @param string sendId * @param int fk_event * @param int reservations * @param int product_id * * @url POST localcreatedpreorderobj * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ function localcreatedpreorderobj($sendId, $fk_event, $reservations, $product_id) { global $user; ApiBbusLog::appLog("{$sendId}: localcreatedpreorderobj"); $apiInvoiceHelper = new ApiInvoiceHelper; $createdPreOrderOBJ = []; for ($i = 1; $i <= $reservations; $i++) { dol_include_once('/comm/action/class/actioncomm.class.php'); dol_include_once('/custom/booking/class/preorder.class.php'); $apiInvoiceHelper->increaseParticipant($fk_event); $lines = $apiInvoiceHelper->getProductLines($product_id); foreach ($lines as $line) { $preOrderObj = new PreOrder($this->db); $preOrderObj->ref = $this->generateRandomString(); $preOrderObj->fk_event = $fk_event; $preOrderObj->fk_product = $product_id; $result = $preOrderObj->create($user); if ($result > 0) { $createdPreOrderOBJ[] = [ 'id' => $preOrderObj->id, 'ref' => $preOrderObj->ref, ]; } else { foreach ($preOrderObj->errors as $error) { ApiBbusLog::appLog("Error: " . $error); } ApiBbusLog::appLog("{$sendId}: Unsaved event: " . $fk_event); throw new RestException(401, 'Unsaved event: ' . $fk_event); } } } ApiBbusLog::appLog(json_encode($createdPreOrderOBJ)); return $createdPreOrderOBJ; } function createLog($sendId, $fk_event, $reservations, $product_id) { /** * LOG SECTION */ ApiBbusLog::eventLog("{$sendId} === NEW INVOICE ==="); dol_syslog("{$sendId} === NEW INVOICE ===", LOG_INFO, 0); ApiBbusLog::eventLog("{$sendId} REQUEST: {$sendId}"); dol_syslog("{$sendId} REQUEST: {$sendId}", LOG_INFO, 0); ApiBbusLog::eventLog("{$sendId} User: {$this->user->firstname} {$this->user->lastname} (ID: {$this->user->id})"); dol_syslog("{$sendId} User: {$this->user->firstname} {$this->user->lastname} (ID: {$this->user->id})", LOG_INFO, 0); ApiBbusLog::eventLog("{$sendId} " . json_encode([ 'fk_event' => $fk_event, 'reservations' => $reservations, 'product_id' => $product_id, 'sendId' => $sendId, ])); } function generateRandomString($length = 10) { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $charactersLength = strlen($characters); $randomString = ''; for ($i = 0; $i < $length; $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; } return $randomString; } /** * Save event data in Bookinghistory * * Return an array with details * * @return array|mixed data without useless information * * @param int $fk_event //selected evet from llx_event * * @url POST saveeventdata * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function saveEventData($fk_event, $fk_facture = null, $fk_eventproduct = null, $ref = null) { ApiBbusLog::appLog("saveEventData"); dol_include_once('/custom/booking/class/bookinghistory.class.php'); dol_include_once('/comm/action/class/actioncomm.class.php'); $actionCommObj = new ActionComm($this->db); $sql = "SELECT fk_element, datep, datep2 FROM " . MAIN_DB_PREFIX . $actionCommObj->table_element . " WHERE id = {$fk_event}"; ApiBbusLog::appLog("{$sql}"); $result = $this->db->query($sql); if ($this->db->num_rows($result) > 0) { $row = $this->db->fetch_object($result); $BookingHistory = new BookingHistory($this->db); $BookingHistory->fk_event = $fk_event; $BookingHistory->fk_facture = $fk_facture; $BookingHistory->fk_event_detail = $row->fk_element; $BookingHistory->fk_eventproduct = $fk_eventproduct; $BookingHistory->date_start = strtotime($row->datep); $BookingHistory->date_end = strtotime($row->datep2); $BookingHistory->invoice_number = $ref; $result = $BookingHistory->create($this->user); if ($result > 0) { ApiBbusLog::appLog("saveEventData: OK {$BookingHistory->id}"); return $BookingHistory->id; } else { foreach ($BookingHistory->errors as $error) { ApiBbusLog::appLog("saveEventData: {$error}"); } ApiBbusLog::appLog("saveEventData: ERROR"); throw new RestException(401, 'Unsaved'); } } else { throw new RestException(401, 'No Event record'); } } /** * Validate and save invoice * * Return an array with details * * @return array|mixed data without useless information * * @param int $type_id Type_id * @param string $sendId sendID * @param array $payment Invoice payment * @param array $preorder preorder * @param array $invoice Invoice * @param string $cardPaymentLog Card payment log data * * @url POST validateandsaveinvoice * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function validateandsaveinvoice(int $type_id, string $sendId, array $payment, array $preorder, array $invoice, string $cardPaymentLog = '') { ApiBbusLog::appLog("validateandsaveinvoice"); global $db, $conf; $server_host = $this->getServerHost($type_id); $createdInvoiceArray = $this->localValidateAndSaveInvoice($invoice, $preorder, $payment, $cardPaymentLog, $sendId, $server_host, $type_id); if ($server_host == $conf->global->LOCAL_SERVER_HOST) { ApiBbusLog::appLog("validateandsaveinvoice: local"); ApiBbusLog::appLog(json_encode($preorder)); foreach ($preorder as $order) { $sql = "SELECT fk_product, fk_event FROM llx_booking_preorder WHERE rowid = {$order}"; ApiBbusLog::appLog("{$sql}"); $data = $db->query($sql); if ($db->num_rows($data) > 0) { while ($row = $db->fetch_object($data)) { ApiBbusLog::appLog("while"); $bookingHistory = $this->saveEventData((int)$row->fk_event, $createdInvoiceArray[0]['invoice']['id'], null, $createdInvoiceArray[0]['invoice']['ref']); ApiBbusLog::appLog("______________________________________________________________________________"); $this->updateBbticket($createdInvoiceArray[0]['invoice'], $bookingHistory); $this->deletePreorder($preorder); } } } } else { ApiBbusLog::appLog("validateandsaveinvoice: curl"); /* #-------------- UPDATE BTICKET -------------- ApiBbusLog::appLog("{$sendId}: Update bbticket with curl"); $invoiceForCurl['id'] = $createdInvoiceArray[0]['invoice']['id']; $invoiceForCurl['ref'] = $createdInvoiceArray[0]['invoice']['ref']; $array['fk_booking_history'] = null; $array['invoice'] = $invoiceForCurl; $updateBBticketPostFields = json_encode($array); ApiBbusLog::appLog("{$updateBBticketPostFields}"); $this->curlRunner('bookingapi/curlUpdateBbticket', $updateBBticketPostFields, 'POST', true); */ foreach ($preorder as $rowid) { $params = compact('rowid'); $postFields = json_encode($params); $preorderArray = $this->curlRunner('bookingapi/curllocalpreorderarray', $postFields, 'POST', true); $saveEventdataPostFields = '{"fk_event":"' . (int)$preorderArray->fk_event . '","ref":"' . $createdInvoiceArray[0]['invoice']['ref'] . '","fk_facture":"' . null . '","fk_eventproduct":"' . null . '"}'; $bookingHistory = $this->curlRunner('bookingapi/saveEventData', $saveEventdataPostFields, 'POST', true); $array['fk_booking_history'] = $bookingHistory; $array['invoice'] = $createdInvoiceArray[0]['invoice']; $updateBBticketPostFields = json_encode($array); $this->curlRunner('bookingapi/curlUpdateBbticket', $updateBBticketPostFields, 'POST', true); ApiBbusLog::appLog("validateandsaveinvoice: {$postFields}"); $this->curlRunner('bookingapi/curlDeletePreorder', $postFields, 'POST', true); } } ApiBbusLog::appLog("validateandsaveinvoice: END"); return $createdInvoiceArray; } public function localValidateAndSaveInvoice($invoice, $preorder, $payment, $cardPaymentLog, $sendId, $server_host, $type_id) { ApiBbusLog::appLog("localValidateAndSaveInvoice"); global $user, $db, $conf; $server_host_curl = false; $createdInvoiceData = []; $apiInvoiceHelper = new ApiInvoiceHelper; $bbusApi = new BBus(); foreach ($preorder as $rowid) { $params = compact('rowid'); $postFields = json_encode($params); if ($server_host == $conf->global->LOCAL_SERVER_HOST) { ApiBbusLog::appLog("localValidateAndSaveInvoice: local"); $preorderArray = $this->localpreorderarray($rowid); } else { ApiBbusLog::appLog("localValidateAndSaveInvoice: curl"); if ($type_id != 5 || $type_id != 3 || $type_id != 4) { $server_host_curl = true; } $preorderArray = $this->curlRunner('bookingapi/curllocalpreorderarray', $postFields, 'POST', true); } $lines = $apiInvoiceHelper->getProductLinesWithoutDiscount($preorderArray->fk_product); //$lines = $apiInvoiceHelper->createLinesForCrossShopping($preorderArray->fk_product); $createdInvoiceArray = $bbusApi->invoice($invoice, $lines, $payment, $cardPaymentLog = '', $sendId = '', $server_host_curl); $createdInvoiceData[] = $createdInvoiceArray; } ApiBbusLog::appLog("Visszaküld"); $asd = json_encode($createdInvoiceData); ApiBbusLog::appLog("{$asd}"); return $createdInvoiceData; } public function localpreorderarray(int $rowid) { global $user, $db; $result = []; $sql = "SELECT fk_product, fk_event FROM llx_booking_preorder WHERE rowid = {$rowid}"; $data = $db->query($sql); if ($db->num_rows($data) > 0) { while ($row = $db->fetch_object($data)) { $result = $row; } } return $result; } /** * curllocalpreorderarray * * Return an array with details * * @return array|mixed data without useless information * * @param int $rowid * * @url POST curllocalpreorderarray * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function curllocalpreorderarray(int $rowid) { global $user, $db; $result = []; $sql = "SELECT fk_product, fk_event FROM llx_booking_preorder WHERE rowid = {$rowid}"; $data = $db->query($sql); if ($db->num_rows($data) > 0) { while ($row = $db->fetch_object($data)) { $result = $row; } } return $result; } public function updateBbticket($invoice, $fk_booking_history = null) { ApiBbusLog::appLog("updateBbticket_________START"); global $user; $sql = "SELECT rowid FROM llx_bbus_bbticket WHERE fk_facture = {$invoice['id']}"; ApiBbusLog::appLog("{$sql}"); $data = $this->db->query($sql); if (!empty($this->db->error())) { ApiBbusLog::appLog("{$this->db->error()}"); throw new RestException(401, "{$this->db->error()}"); } $addedsql = $fk_booking_history == null ? "" : " booking_history_id = {$fk_booking_history},"; if ($this->db->num_rows($data) > 0) { while ($row = $this->db->fetch_object($data)) { $sql = "UPDATE llx_bbus_bbticket SET {$addedsql} invoice_number = '{$invoice['ref']}' WHERE rowid = {$row->rowid}"; ApiBbusLog::appLog("{$sql}"); $this->db->query($sql); } } else { ApiBbusLog::appLog("{$this->db->error()}"); ApiBbusLog::appLog("api_booking (Excelia): updateBbticket error"); } } /** * curlupdateBbticket * * Return an array with details * * * @param array $invoice invoice * * @url POST curlupdateBbticket * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function curlupdateBbticket(array $invoice, $fk_booking_history = null) { global $user; ApiBbusLog::appLog("curlupdateBbticket______START"); $sql = "SELECT rowid FROM llx_bbus_bbticket WHERE invoice_number = '{$invoice['id']}' OR invoice_number = '{$invoice['ref']}'"; ApiBbusLog::appLog("{$sql}"); $data = $this->db->query($sql); if (!empty($this->db->error())) { ApiBbusLog::appLog("{$this->db->error()}"); throw new RestException(401, "{$this->db->error()}"); } if ($this->db->num_rows($data) > 0) { while ($row = $this->db->fetch_object($data)) { $booking_history_id = is_null($fk_booking_history) ? 'booking_history_id = null,' : 'booking_history_id = ' . $fk_booking_history . ','; $sql = "UPDATE llx_bbus_bbticket SET {$booking_history_id} invoice_number = '{$invoice['ref']}' WHERE rowid = {$row->rowid}"; ApiBbusLog::appLog("{$sql}"); $this->db->query($sql); } } else { ApiBbusLog::appLog("api_booking: curlupdateBbticket error"); ApiBbusLog::appLog("{$this->db->error()}"); throw new RestException(401, 'api_booking: curlupdateBbticket error'); } } public function deletePreorder($preorder) { foreach ($preorder as $item) { $sql = "DELETE FROM llx_booking_preorder WHERE rowid = {$item}"; $this->db->query($sql); } } /** * curldeletePreorder * * Return an array with details * * * @param array $preorder preorder * * @url POST curldeletePreorder * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function curldeletePreorder($preorder) { foreach ($preorder as $item) { $sql = "DELETE FROM llx_booking_preorder WHERE rowid = {$item}"; $this->db->query($sql); } } public function eventErasuer() { global $user; $limitDate = date("Y-m-d H:i:s", dol_now() - 1200); $sql = "SELECT * FROM llx_booking_preorder WHERE date_creation < '{$limitDate}'"; //print $sql;exit; $data = $this->db->query($sql); if ($this->db->num_rows($data) > 0) { while ($row = $this->db->fetch_object($data)) { //print_r($row); $actioncommObj = new ActionComm($this->db); $resultActionComm = $actioncommObj->fetch($row->fk_event); $actioncommObj->array_options['options_participants'] = $actioncommObj->array_options['options_participants'] - 1; $actioncommObj->update($user); } //exit; $sqlDelete = "DELETE FROM llx_booking_preorder WHERE date_creation < '{$limitDate}'"; $dataDelete = $this->db->query($sqlDelete); } } /** * Increase Participants * * Return an array with details * * @return array|mixed data without useless information * * @param int $event_id //rowid of evet from llx_event * * @url POST increaseparticipant * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function increaseParticipant(int $event_id) { dol_include_once('/comm/action/class/actioncomm.class.php'); global $user; $actionComObj = new ActionComm($this->db); $actionComObj->fetch($event_id); $max_num = (int) $actionComObj->array_options['options_max_num']; $buffer = (int) $actionComObj->array_options['options_buffer']; $participants = (int) $actionComObj->array_options['options_participants']; $increasedParticipants = $participants + 1; if ($increasedParticipants > ($max_num + $buffer)) { return 'full'; } elseif (($increasedParticipants > $max_num) && ($increasedParticipants <= ($max_num + $buffer))) { $actionComObj->array_options['options_participants'] = $increasedParticipants; $actionComObj->update($user); return 'Buffer'; } else { $actionComObj->array_options['options_participants'] = $increasedParticipants; $actionComObj->update($user); return 'OK'; } } /** * Reduce Participants * * Return an array with details * * @return array|mixed data without useless information * * @param int $event_id //rowid of evet from llx_event * * @url POST reduceparticipant * * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function reduceParticipant(int $event_id) { global $user; dol_include_once('/comm/action/class/actioncomm.class.php'); $actionComObj = new ActionComm($this->db); $actionComObj->fetch($event_id); $participants = (int) $actionComObj->array_options['options_participants']; $reducedParticipants = $participants - 1; $actionComObj->array_options['options_participants'] = $reducedParticipants < 0 ? 0 : $reducedParticipants; $actionComObj->update($user); return 'OK'; } /** * Create invoice * * @param array $invoice Invoice data * @param array $lines Invoice lines * @param array $payment Invoice payment * @param string $eventid Event ID * @param string $cardPaymentLog Card payment log data * * @access protected * @return array|mixed Data without useless information * * @url POST /invoice */ public function invoice(array $invoice, array $lines, array $payment, string $cardPaymentLog = '', string $eventid = '', string $sendId = '') { if (empty($sendId)) { $sendId = substr(md5(rand()), 0, 8); } /** * LOG SECTION */ ApiBbusLog::appLog("{$sendId} === NEW INVOICE ==="); dol_syslog("{$sendId} === NEW INVOICE ===", LOG_INFO, 0); ApiBbusLog::appLog("{$sendId} REQUEST: {$sendId}"); dol_syslog("{$sendId} REQUEST: {$sendId}", LOG_INFO, 0); ApiBbusLog::appLog("{$sendId} User: {$this->user->firstname} {$this->user->lastname} (ID: {$this->user->id})"); dol_syslog("{$sendId} User: {$this->user->firstname} {$this->user->lastname} (ID: {$this->user->id})", LOG_INFO, 0); ApiBbusLog::appLog("{$sendId} " . json_encode([ 'invoice' => $invoice, 'lines' => $lines, 'payment' => $payment, 'eventid' => $eventid, 'cardPaymentLog' => $cardPaymentLog ])); /** * CHECK RIGHTS */ if (!DolibarrApiAccess::$user->rights->facture->creer) { ApiBbusLog::appLog("{$sendId} Insufficient rights"); throw new RestException(401, 'Insufficient rights'); } $apiInvoiceHelper = new ApiInvoiceHelper; /** * handle invoice basic data */ $invoiceObj = $apiInvoiceHelper->createInvoice($invoice, $sendId); /** * handle invoice line(s) */ $products = []; //print $eventid;exit; foreach ($lines as $line) { $invoiceObj = $apiInvoiceHelper->addLineToInvoice($invoiceObj, $line, $sendId); $this->increaseParticipant((int) $eventid); $product = $apiInvoiceHelper->loadProductToResult($line); if (!empty($product)) { $products[] = $product; } } $invoiceObj->fetch_lines(); foreach ($products as &$row) { foreach ($invoiceObj->lines as $line) { if ($line->product_ref == $row['ref']) { //$row['price'] = $line->total_ttc; $row['price'] = $line->multicurrency_total_ttc; $row['total_tva'] = $line->total_tva; //$row['total_tva'] = $line->multicurrency_total_tva; } } } return [ 'sendId' => $sendId, 'invoice' => [ 'id' => $invoiceObj->id, 'ref' => $invoiceObj->ref, 'total' => $invoiceObj->multicurrency_total_ttc ], 'products' => $products, ]; } /** * Get all free spaces on a selected date * * Return an array with details * * @return array|mixed data without useless information * * @param int type_id * @param string date_from * @param string date_to * @param int product_id * @param int participant_number * * @url POST getavailablespaces * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function getAvailableSpaces(int $type_id, string $date_from, string $date_to, int $product_id, int $participant_number) { global $conf; $server_host = $this->getServerHost($type_id); if ($server_host == $conf->global->LOCAL_SERVER_HOST) { ApiBbusLog::appLog("getAvailableSpaces: local"); return $this->localAvailablePlaces($date_from, $date_to, $product_id, $participant_number); } else { ApiBbusLog::appLog("getAvailableSpaces: curl"); $params = compact('date_from', 'date_to', 'product_id', 'participant_number'); $postFields = json_encode($params); return $this->curlRunner('bookingapi/localavailableplaces', $postFields, 'POST', true); } } /* private function getServerHost($type_id) { $basicServices = new BasicServices($this->db); $resultBS = $basicServices->fetch($type_id); return $basicServices->server_host; } */ /** * Get all free spaces on a selected date * * Return an array with details * * @return array|mixed data without useless information * * @param string date_from * @param string date_to * @param int product_id * @param int participant_number * * @url POST localavailableplaces * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function localavailableplaces($date_from, $date_to, $product_id, $participant_number) { ApiBbusLog::appLog("localAvailablePlaces"); $array = []; $date = new DateTime($date_to); $date->modify('+1 day'); $date_to = $date->format('Y-m-d'); $date_from = $this->getCutOffTimeDate($product_id); $sql = "SELECT ac.id, ac.datep, ace.max_num, ace.participants, ace.buffer FROM llx_actioncomm as ac LEFT JOIN llx_actioncomm_extrafields as ace ON ace.fk_object = ac.id INNER JOIN llx_eventwizard_eventdetails as ed ON ac.fk_element = ed.rowid INNER JOIN llx_eventwizard_eventproduct as ep ON ep.fk_eventdetails = ac.fk_element WHERE ac.code = 'AC_EVENT' AND ep.fk_product = {$product_id} AND ac.datep > '{$date_from}' AND ac.datep2 < '{$date_to} 00:00:00' ORDER BY ac.datep ASC"; $result = $this->db->query($sql); while ($row = $this->db->fetch_object($result)) { $max_num = $row->max_num; $buffer = $row->buffer; $participants = $row->participants; $date = strtotime($row->datep); $element['event_id'] = $row->id; //$element['date'] = date("Y-m-d", $date); //$element['time'] = date("H:i", $date); $element['participants'] = is_null($participants) ? 0 : (int) $participants; $element['max_num'] = $max_num; $element['buffer'] = $buffer; if ($max_num > $participants) { if ($max_num + $buffer > $participants + $participant_number) { $array[date("Y-m-d", $date)][date("H:i", $date)] = $element; } } } return $array; } /** * localCheckAvailablePlaces * * * @return array|mixed data without useless information * * @param int product_id * * @url POST localCheckAvailablePlaces * * @throws RestException 401 Not allowed * @throws RestException 404 Not found */ public function localCheckAvailablePlaces($product_id) { $date_from = $this->getCutOffTimeDate($product_id); $sql = "SELECT ac.id, ac.datep, ace.max_num, ace.participants, ace.buffer FROM llx_actioncomm as ac LEFT JOIN llx_actioncomm_extrafields as ace ON ace.fk_object = ac.id INNER JOIN llx_eventwizard_eventdetails as ed ON ac.fk_element = ed.rowid INNER JOIN llx_eventwizard_eventproduct as ep ON ep.fk_eventdetails = ac.fk_element WHERE ac.code = 'AC_EVENT' AND ep.fk_product = {$product_id} AND ac.datep > '{$date_from}' ORDER BY ac.datep ASC"; $result = $this->db->query($sql); return $this->db->num_rows($result) > 0; } private function getCutOffTimeDate($product_id) { $date = date("Y-m-d H:i:s"); ApiBbusLog::appLog("{$date}"); $sql = "SELECT cut_off_time_app FROM llx_product_extrafields WHERE fk_object = {$product_id}"; if ($result = $this->db->query($sql)) { if ($this->db->num_rows($result) > 0) { while ($row = $this->db->fetch_object($result)) { $cutOffTime = $row->cut_off_time_app; } $dateTimestamp = strtotime($date); $date = date("Y-m-d H:i:s", $dateTimestamp + $cutOffTime * 60); } } ApiBbusLog::appLog("{$date}"); return $date; } private function noLocalEmptySpaces($event_id, $reservations) { $sql = "SELECT ace.max_num, ace.buffer, ace.participants FROM llx_actioncomm as ac INNER JOIN llx_actioncomm_extrafields as ace ON ace.fk_object = ac.id WHERE ac.id = {$event_id}"; $result = $this->db->query($sql); if ($this->db->num_rows($result) == 0) { return true; } while ($row = $this->db->fetch_object($result)) { if ($row->participants >= $row->max_num) { return true; } if ($row->participants + $reservations > $row->max_num + $row->buffer) { return true; } } return false; } /** * curlnoLocalEmptySpaces * * Return an array with details * * @return array|mixed data without useless information * * @param int $fk_event * @param int $reservations * * @url POST curlnoLocalEmptySpaces * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function curlnoLocalEmptySpaces($fk_event, $reservations) { ApiBbusLog::appLog("curlnoLocalEmptySpaces"); $sql = "SELECT ace.max_num, ace.buffer, ace.participants FROM llx_actioncomm as ac INNER JOIN llx_actioncomm_extrafields as ace ON ace.fk_object = ac.id WHERE ac.id = {$fk_event}"; $result = $this->db->query($sql); if ($this->db->num_rows($result) == 0) { return true; } while ($row = $this->db->fetch_object($result)) { if ($row->participants >= $row->max_num) { return true; } if ($row->participants + $reservations > $row->max_num + $row->buffer) { return true; } } return false; } /** * curlcreatedpreorderobj * * Return an array with details * * @return array|mixed data without useless information * * @param string $sendId * @param int $fk_event * @param int $reservations * @param int $product_id * * @url POST curlcreatedpreorderobj * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ function curlcreatedpreorderobj($sendId, $fk_event, $reservations, $product_id) { ApiBbusLog::appLog("curlcreatedpreorderobj"); $apiInvoiceHelper = new ApiInvoiceHelper; $createdPreOrderOBJ = []; for ($i = 1; $i <= $reservations; $i++) { dol_include_once('/comm/action/class/actioncomm.class.php'); dol_include_once('/custom/booking/class/preorder.class.php'); $apiInvoiceHelper->increaseParticipant($fk_event); $lines = $apiInvoiceHelper->getProductLines($product_id); foreach ($lines as $line) { $preOrderObj = new PreOrder($this->db); $preOrderObj->ref = $this->generateRandomString(); $preOrderObj->fk_event = $fk_event; $preOrderObj->fk_product = $product_id; $result = $preOrderObj->create($this->user); if ($result > 0) { $createdPreOrderOBJ[] = [ 'id' => $preOrderObj->id, 'ref' => $preOrderObj->ref, ]; } else { ApiBbusLog::appLog(json_encode(['error' => $preOrderObj->errors])); ApiBbusLog::appLog("Unsaved event: " . $fk_event); throw new RestException(401, 'Unsaved event: ' . $fk_event); } } } ApiBbusLog::appLog(json_encode($createdPreOrderOBJ)); return $createdPreOrderOBJ; } /** * Create BBticket record * * Return an array with details * * @return array|mixed data without useless information * * @param string $product_id * @param string $datec * @param string $facture_id * * @url POST createbbticket * @access protected * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function createbbticket(string $product_id, string $datec, string $facture_id) { global $db, $conf; $sql = "SELECT validperiod, occasions FROM " . $this->db->prefix() . "product_extrafields WHERE fk_object = " . $product_id; $result = $db->query($sql); while ($sqlDataResult = $db->fetch_array($result)) { $validperiod = $sqlDataResult['validperiod'] ? $sqlDataResult['validperiod'] : 366; $occasions = $sqlDataResult['occasions']; } $ticket = new BbTicket($db); $ticket->fk_facture = null; $ticket->bundle_id = $product_id; $ticket->usable_occasions = $occasions; $ticket->usage = '0'; $ticket->available_at = $this->getAvailableAtDate($datec, $validperiod); $ticket->ticket_id = $product_id; $ticket->fk_settlements_group_id = $conf->global->CURL_GROUP_ID; $ticket->invoice_number = $facture_id; if ($ticket->create($this->user) == -1) { dol_syslog("Nem sikerult a ticketek mentese"); } return true; } private function getAvailableAtDate($date, $validperiod) { $available_at = date('Y-m-d H:i:s', strtotime($date . ' +' . $validperiod . ' days')); return $available_at; } /** * Get printed factures refs * * Return an array with details * * @return array|mixed data without useless information * * @param string $from * @param string $to * @param string $factures * * @url POST getPrintedFacturesRefs * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function getPrintedFacturesRefs(string $from, string $to, string $factures = "") { $refs = ""; if ($factures != "") { global $db, $conf; $curluser_id = $conf->global->CURL_USER_ID; $sql = "SELECT bbt.invoice_number FROM llx_bbus_bbticket as bbt INNER JOIN llx_bbus_bbticketinvoiceprinting as bbtip ON bbtip.ticket_id = bbt.rowid WHERE bbt.fk_user_creat = {$curluser_id} AND bbt.invoice_number IN ({$factures}) AND bbt.date_creation BETWEEN '{$from}' AND '{$to}' ORDER BY bbt.rowid DESC"; $result = $db->query($sql); if ($db->num_rows($result) > 0) { while ($row = $db->fetch_object($result)) { if ($refs == "") { $refs .= "'" . $row->invoice_number . "'"; } else { $refs .= ",'" . $row->invoice_number . "'"; } } } } return $refs; } /** * Get printed factures refs in array * * Return an array with details * * @return array|mixed data without useless information * * @param string $from * @param string $to * * @url POST getPrintedFacturesRefsArray * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function getPrintedFacturesRefsArray(string $from, string $to, string $factures = '') { $refs = []; if ($factures !== '') { global $db, $conf; $curluser_id = $conf->global->CURL_USER_ID; $sql = "SELECT bbt.invoice_number FROM llx_bbus_bbticket as bbt INNER JOIN llx_bbus_bbticketinvoiceprinting as bbtip ON bbtip.ticket_id = bbt.rowid WHERE bbt.fk_user_creat = {$curluser_id} AND bbt.invoice_number IN ({$factures}) AND bbt.date_creation BETWEEN '{$from}' AND '{$to}' ORDER BY bbt.rowid DESC"; $result = $db->query($sql); if ($db->num_rows($result) > 0) { while ($row = $db->fetch_object($result)) { $refs[] = $row->invoice_number; } } } return $refs; } /** * Get number of a facture's printer count * * Return an array with details * * @return array|mixed data without useless information * * @param string $invoice_number * * @url POST getFacturesNumberOfPrints * @throws RestException 401 Not allowed * @throws RestException 404 Not found * @throws RestException 506 No available spaces * */ public function getFacturesNumberOfPrints(string $invoice_number) { $number = 0; global $db; $sql = "SELECT tip.fk_facture, tip.product_id, count(tip.product_id) FROM llx_bbus_bbticketinvoiceprinting as tip WHERE tip.invoice_number = '{$invoice_number}' GROUP BY tip.fk_facture, tip.product_id"; $result = $db->query($sql); if ($db->num_rows($result) > 0) { while ($row = $db->fetch_object($result)) { $array[$row->fk_facture][$row->product_id] = $row->count; } $firstKey = array_key_first($array); $firstValue = $array[$firstKey]; $number = reset($firstValue); } return $number; } }