api_products.class.php 65 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108
  1. <?php
  2. /* Copyright (C) 2015 Jean-François Ferry <jfefe@aternatik.fr>
  3. * Copyright (C) 2019 Cedric Ancelin <icedo.anc@gmail.com>
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use Luracast\Restler\RestException;
  19. require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
  20. require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
  21. require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
  22. require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
  23. require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
  24. require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
  25. require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
  26. /**
  27. * API class for products
  28. *
  29. * @access protected
  30. * @class DolibarrApiAccess {@requires user,external}
  31. */
  32. class Products extends DolibarrApi
  33. {
  34. /**
  35. * @var array $FIELDS Mandatory fields, checked when create and update object
  36. */
  37. public static $FIELDS = array(
  38. 'ref',
  39. 'label'
  40. );
  41. /**
  42. * @var Product $product {@type Product}
  43. */
  44. public $product;
  45. /**
  46. * @var ProductFournisseur $productsupplier {@type ProductFournisseur}
  47. */
  48. public $productsupplier;
  49. /**
  50. * Constructor
  51. */
  52. public function __construct()
  53. {
  54. global $db, $conf;
  55. $this->db = $db;
  56. $this->product = new Product($this->db);
  57. $this->productsupplier = new ProductFournisseur($this->db);
  58. }
  59. /**
  60. * Get properties of a product object by id
  61. *
  62. * Return an array with product information.
  63. *
  64. * @param int $id ID of product
  65. * @param int $includestockdata Load also information about stock (slower)
  66. * @param bool $includesubproducts Load information about subproducts
  67. * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product)
  68. * @param bool $includetrans Load also the translations of product label and description
  69. * @return array|mixed Data without useless information
  70. *
  71. * @throws RestException 401
  72. * @throws RestException 403
  73. * @throws RestException 404
  74. */
  75. public function get($id, $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includetrans = false)
  76. {
  77. return $this->_fetch($id, '', '', '', $includestockdata, $includesubproducts, $includeparentid, false, $includetrans);
  78. }
  79. /**
  80. * Get properties of a product object by ref
  81. *
  82. * Return an array with product information.
  83. *
  84. * @param string $ref Ref of element
  85. * @param int $includestockdata Load also information about stock (slower)
  86. * @param bool $includesubproducts Load information about subproducts
  87. * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product)
  88. * @param bool $includetrans Load also the translations of product label and description
  89. *
  90. * @return array|mixed Data without useless information
  91. *
  92. * @url GET ref/{ref}
  93. *
  94. * @throws RestException 401
  95. * @throws RestException 403
  96. * @throws RestException 404
  97. */
  98. public function getByRef($ref, $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includetrans = false)
  99. {
  100. return $this->_fetch('', $ref, '', '', $includestockdata, $includesubproducts, $includeparentid, false, $includetrans);
  101. }
  102. /**
  103. * Get properties of a product object by ref_ext
  104. *
  105. * Return an array with product information.
  106. *
  107. * @param string $ref_ext Ref_ext of element
  108. * @param int $includestockdata Load also information about stock (slower)
  109. * @param bool $includesubproducts Load information about subproducts
  110. * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product)
  111. * @param bool $includetrans Load also the translations of product label and description
  112. *
  113. * @return array|mixed Data without useless information
  114. *
  115. * @url GET ref_ext/{ref_ext}
  116. *
  117. * @throws RestException 401
  118. * @throws RestException 403
  119. * @throws RestException 404
  120. */
  121. public function getByRefExt($ref_ext, $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includetrans = false)
  122. {
  123. return $this->_fetch('', '', $ref_ext, '', $includestockdata, $includesubproducts, $includeparentid, false, $includetrans);
  124. }
  125. /**
  126. * Get properties of a product object by barcode
  127. *
  128. * Return an array with product information.
  129. *
  130. * @param string $barcode Barcode of element
  131. * @param int $includestockdata Load also information about stock (slower)
  132. * @param bool $includesubproducts Load information about subproducts
  133. * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product)
  134. * @param bool $includetrans Load also the translations of product label and description
  135. *
  136. * @return array|mixed Data without useless information
  137. *
  138. * @url GET barcode/{barcode}
  139. *
  140. * @throws RestException 401
  141. * @throws RestException 403
  142. * @throws RestException 404
  143. */
  144. public function getByBarcode($barcode, $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includetrans = false)
  145. {
  146. return $this->_fetch('', '', '', $barcode, $includestockdata, $includesubproducts, $includeparentid, false, $includetrans);
  147. }
  148. /**
  149. * List products
  150. *
  151. * Get a list of products
  152. *
  153. * @param string $sortfield Sort field
  154. * @param string $sortorder Sort order
  155. * @param int $limit Limit for list
  156. * @param int $page Page number
  157. * @param int $mode Use this param to filter list (0 for all, 1 for only product, 2 for only service)
  158. * @param int $category Use this param to filter list by category
  159. * @param string $sqlfilters Other criteria to filter answers separated by a comma. Syntax example "(t.tobuy:=:0) and (t.tosell:=:1)"
  160. * @param bool $ids_only Return only IDs of product instead of all properties (faster, above all if list is long)
  161. * @param int $variant_filter Use this param to filter list (0 = all, 1=products without variants, 2=parent of variants, 3=variants only)
  162. * @param bool $pagination_data If this parameter is set to true the response will include pagination data. Default value is false. Page starts from 0
  163. * @param int $includestockdata Load also information about stock (slower)
  164. * @param string $properties Restrict the data returned to theses properties. Ignored if empty. Comma separated list of properties names
  165. * @return array Array of product objects
  166. */
  167. public function index($sortfield = "t.ref", $sortorder = 'ASC', $limit = 100, $page = 0, $mode = 0, $category = 0, $sqlfilters = '', $ids_only = false, $variant_filter = 0, $pagination_data = false, $includestockdata = 0, $properties = '')
  168. {
  169. global $db, $conf;
  170. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  171. throw new RestException(403);
  172. }
  173. $obj_ret = array();
  174. $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : '';
  175. $sql = "SELECT t.rowid, t.ref, t.ref_ext";
  176. $sql .= " FROM ".$this->db->prefix()."product as t";
  177. $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_extrafields AS ef ON ef.fk_object = t.rowid"; // So we will be able to filter on extrafields
  178. if ($category > 0) {
  179. $sql .= ", ".$this->db->prefix()."categorie_product as c";
  180. }
  181. $sql .= ' WHERE t.entity IN ('.getEntity('product').')';
  182. if ($variant_filter == 1) {
  183. $sql .= ' AND t.rowid not in (select distinct fk_product_parent from '.$this->db->prefix().'product_attribute_combination)';
  184. $sql .= ' AND t.rowid not in (select distinct fk_product_child from '.$this->db->prefix().'product_attribute_combination)';
  185. }
  186. if ($variant_filter == 2) {
  187. $sql .= ' AND t.rowid in (select distinct fk_product_parent from '.$this->db->prefix().'product_attribute_combination)';
  188. }
  189. if ($variant_filter == 3) {
  190. $sql .= ' AND t.rowid in (select distinct fk_product_child from '.$this->db->prefix().'product_attribute_combination)';
  191. }
  192. // Select products of given category
  193. if ($category > 0) {
  194. $sql .= " AND c.fk_categorie = ".((int) $category);
  195. $sql .= " AND c.fk_product = t.rowid";
  196. }
  197. if ($mode == 1) {
  198. // Show only products
  199. $sql .= " AND t.fk_product_type = 0";
  200. } elseif ($mode == 2) {
  201. // Show only services
  202. $sql .= " AND t.fk_product_type = 1";
  203. }
  204. // Add sql filters
  205. if ($sqlfilters) {
  206. $errormessage = '';
  207. $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
  208. if ($errormessage) {
  209. throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage);
  210. }
  211. }
  212. //this query will return total products with the filters given
  213. $sqlTotals = str_replace('SELECT t.rowid, t.ref, t.ref_ext', 'SELECT count(t.rowid) as total', $sql);
  214. $sql .= $this->db->order($sortfield, $sortorder);
  215. if ($limit) {
  216. if ($page < 0) {
  217. $page = 0;
  218. }
  219. $offset = $limit * $page;
  220. $sql .= $this->db->plimit($limit + 1, $offset);
  221. }
  222. $result = $this->db->query($sql);
  223. if ($result) {
  224. $num = $this->db->num_rows($result);
  225. $min = min($num, ($limit <= 0 ? $num : $limit));
  226. $i = 0;
  227. while ($i < $min) {
  228. $obj = $this->db->fetch_object($result);
  229. if (!$ids_only) {
  230. $product_static = new Product($this->db);
  231. if ($product_static->fetch($obj->rowid)) {
  232. if (!empty($includestockdata) && DolibarrApiAccess::$user->rights->stock->lire) {
  233. $product_static->load_stock();
  234. if (is_array($product_static->stock_warehouse)) {
  235. foreach ($product_static->stock_warehouse as $keytmp => $valtmp) {
  236. if (isset($product_static->stock_warehouse[$keytmp]->detail_batch) && is_array($product_static->stock_warehouse[$keytmp]->detail_batch)) {
  237. foreach ($product_static->stock_warehouse[$keytmp]->detail_batch as $keytmp2 => $valtmp2) {
  238. unset($product_static->stock_warehouse[$keytmp]->detail_batch[$keytmp2]->db);
  239. }
  240. }
  241. }
  242. }
  243. }
  244. $obj_ret[] = $this->_filterObjectProperties($this->_cleanObjectDatas($product_static), $properties);
  245. }
  246. } else {
  247. $obj_ret[] = $obj->rowid;
  248. }
  249. $i++;
  250. }
  251. } else {
  252. throw new RestException(503, 'Error when retrieve product list : '.$this->db->lasterror());
  253. }
  254. //if $pagination_data is true the response will contain element data with all values and element pagination with pagination data(total,page,limit)
  255. if ($pagination_data) {
  256. $totalsResult = $this->db->query($sqlTotals);
  257. $total = $this->db->fetch_object($totalsResult)->total;
  258. $tmp = $obj_ret;
  259. $obj_ret = array();
  260. $obj_ret['data'] = $tmp;
  261. $obj_ret['pagination'] = array(
  262. 'total' => (int) $total,
  263. 'page' => $page, //count starts from 0
  264. 'page_count' => ceil((int) $total/$limit),
  265. 'limit' => $limit
  266. );
  267. }
  268. return $obj_ret;
  269. }
  270. /**
  271. * Create product object
  272. *
  273. * @param array $request_data Request data
  274. * @return int ID of product
  275. */
  276. public function post($request_data = null)
  277. {
  278. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  279. throw new RestException(401);
  280. }
  281. // Check mandatory fields
  282. $result = $this->_validate($request_data);
  283. foreach ($request_data as $field => $value) {
  284. if ($field === 'caller') {
  285. // Add a mention of caller so on trigger called after action, we can filter to avoid a loop if we try to sync back again whith the caller
  286. $this->product->context['caller'] = $request_data['caller'];
  287. continue;
  288. }
  289. $this->product->$field = $value;
  290. }
  291. if ($this->product->create(DolibarrApiAccess::$user) < 0) {
  292. throw new RestException(500, "Error creating product", array_merge(array($this->product->error), $this->product->errors));
  293. }
  294. return $this->product->id;
  295. }
  296. /**
  297. * Update product.
  298. * Price will be updated by this API only if option is set on "One price per product". See other APIs for other price modes.
  299. *
  300. * @param int $id Id of product to update
  301. * @param array $request_data Datas
  302. * @return int
  303. *
  304. * @throws RestException 401
  305. * @throws RestException 404
  306. */
  307. public function put($id, $request_data = null)
  308. {
  309. global $conf;
  310. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  311. throw new RestException(401);
  312. }
  313. $result = $this->product->fetch($id);
  314. if (!$result) {
  315. throw new RestException(404, 'Product not found');
  316. }
  317. if (!DolibarrApi::_checkAccessToResource('product', $this->product->id)) {
  318. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  319. }
  320. $oldproduct = dol_clone($this->product);
  321. foreach ($request_data as $field => $value) {
  322. if ($field == 'id') {
  323. continue;
  324. }
  325. if ($field == 'stock_reel') {
  326. throw new RestException(400, 'Stock reel cannot be updated here. Use the /stockmovements endpoint instead');
  327. }
  328. if ($field === 'caller') {
  329. // Add a mention of caller so on trigger called after action, we can filter to avoid a loop if we try to sync back again whith the caller
  330. $this->product->context['caller'] = $request_data['caller'];
  331. continue;
  332. }
  333. $this->product->$field = $value;
  334. }
  335. $updatetype = false;
  336. if ($this->product->type != $oldproduct->type && ($this->product->isProduct() || $this->product->isService())) {
  337. $updatetype = true;
  338. }
  339. $result = $this->product->update($id, DolibarrApiAccess::$user, 1, 'update', $updatetype);
  340. // If price mode is 1 price per product
  341. if ($result > 0 && getDolGlobalString('PRODUCT_PRICE_UNIQ')) {
  342. // We update price only if it was changed
  343. $pricemodified = false;
  344. if ($this->product->price_base_type != $oldproduct->price_base_type) {
  345. $pricemodified = true;
  346. } else {
  347. if ($this->product->tva_tx != $oldproduct->tva_tx) {
  348. $pricemodified = true;
  349. }
  350. if ($this->product->tva_npr != $oldproduct->tva_npr) {
  351. $pricemodified = true;
  352. }
  353. if ($this->product->default_vat_code != $oldproduct->default_vat_code) {
  354. $pricemodified = true;
  355. }
  356. if ($this->product->price_base_type == 'TTC') {
  357. if ($this->product->price_ttc != $oldproduct->price_ttc) {
  358. $pricemodified = true;
  359. }
  360. if ($this->product->price_min_ttc != $oldproduct->price_min_ttc) {
  361. $pricemodified = true;
  362. }
  363. } else {
  364. if ($this->product->price != $oldproduct->price) {
  365. $pricemodified = true;
  366. }
  367. if ($this->product->price_min != $oldproduct->price_min) {
  368. $pricemodified = true;
  369. }
  370. }
  371. }
  372. if ($pricemodified) {
  373. $newvat = $this->product->tva_tx;
  374. $newnpr = $this->product->tva_npr;
  375. $newvatsrccode = $this->product->default_vat_code;
  376. $newprice = $this->product->price;
  377. $newpricemin = $this->product->price_min;
  378. if ($this->product->price_base_type == 'TTC') {
  379. $newprice = $this->product->price_ttc;
  380. $newpricemin = $this->product->price_min_ttc;
  381. }
  382. $result = $this->product->updatePrice($newprice, $this->product->price_base_type, DolibarrApiAccess::$user, $newvat, $newpricemin, 0, $newnpr, 0, 0, array(), $newvatsrccode);
  383. }
  384. }
  385. if ($result <= 0) {
  386. throw new RestException(500, "Error updating product", array_merge(array($this->product->error), $this->product->errors));
  387. }
  388. return $this->get($id);
  389. }
  390. /**
  391. * Delete product
  392. *
  393. * @param int $id Product ID
  394. * @return array
  395. */
  396. public function delete($id)
  397. {
  398. if (!DolibarrApiAccess::$user->rights->produit->supprimer) {
  399. throw new RestException(401);
  400. }
  401. $result = $this->product->fetch($id);
  402. if (!$result) {
  403. throw new RestException(404, 'Product not found');
  404. }
  405. if (!DolibarrApi::_checkAccessToResource('product', $this->product->id)) {
  406. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  407. }
  408. // The Product::delete() method uses the global variable $user.
  409. global $user;
  410. $user = DolibarrApiAccess::$user;
  411. $res = $this->product->delete(DolibarrApiAccess::$user);
  412. if ($res < 0) {
  413. throw new RestException(500, "Can't delete, error occurs");
  414. } elseif ($res == 0) {
  415. throw new RestException(409, "Can't delete, that product is probably used");
  416. }
  417. return array(
  418. 'success' => array(
  419. 'code' => 200,
  420. 'message' => 'Object deleted'
  421. )
  422. );
  423. }
  424. /**
  425. * Get the list of subproducts of the product.
  426. *
  427. * @param int $id Id of parent product/service
  428. * @return array
  429. *
  430. * @throws RestException
  431. * @throws RestException 401
  432. * @throws RestException 404
  433. *
  434. * @url GET {id}/subproducts
  435. */
  436. public function getSubproducts($id)
  437. {
  438. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  439. throw new RestException(401);
  440. }
  441. if (!DolibarrApi::_checkAccessToResource('product', $id)) {
  442. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  443. }
  444. $childsArbo = $this->product->getChildsArbo($id, 1);
  445. $keys = array('rowid', 'qty', 'fk_product_type', 'label', 'incdec', 'ref', 'fk_association', 'rang');
  446. $childs = array();
  447. foreach ($childsArbo as $values) {
  448. $childs[] = array_combine($keys, $values);
  449. }
  450. return $childs;
  451. }
  452. /**
  453. * Add subproduct.
  454. *
  455. * Link a product/service to a parent product/service
  456. *
  457. * @param int $id Id of parent product/service
  458. * @param int $subproduct_id Id of child product/service
  459. * @param int $qty Quantity
  460. * @param int $incdec 1=Increase/decrease stock of child when parent stock increase/decrease
  461. * @return int
  462. *
  463. * @throws RestException
  464. * @throws RestException 401
  465. * @throws RestException 404
  466. *
  467. * @url POST {id}/subproducts/add
  468. */
  469. public function addSubproducts($id, $subproduct_id, $qty, $incdec = 1)
  470. {
  471. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  472. throw new RestException(401);
  473. }
  474. if (!DolibarrApi::_checkAccessToResource('product', $id)) {
  475. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  476. }
  477. $result = $this->product->add_sousproduit($id, $subproduct_id, $qty, $incdec);
  478. if ($result <= 0) {
  479. throw new RestException(500, "Error adding product child");
  480. }
  481. return $result;
  482. }
  483. /**
  484. * Remove subproduct.
  485. * Unlink a product/service from a parent product/service
  486. *
  487. * @param int $id Id of parent product/service
  488. * @param int $subproduct_id Id of child product/service
  489. * @return int
  490. *
  491. * @throws RestException 401
  492. * @throws RestException 404
  493. *
  494. * @url DELETE {id}/subproducts/remove/{subproduct_id}
  495. */
  496. public function delSubproducts($id, $subproduct_id)
  497. {
  498. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  499. throw new RestException(401);
  500. }
  501. if (!DolibarrApi::_checkAccessToResource('product', $id)) {
  502. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  503. }
  504. $result = $this->product->del_sousproduit($id, $subproduct_id);
  505. if ($result <= 0) {
  506. throw new RestException(500, "Error while removing product child");
  507. }
  508. return $result;
  509. }
  510. /**
  511. * Get categories for a product
  512. *
  513. * @param int $id ID of product
  514. * @param string $sortfield Sort field
  515. * @param string $sortorder Sort order
  516. * @param int $limit Limit for list
  517. * @param int $page Page number
  518. *
  519. * @return mixed
  520. *
  521. * @url GET {id}/categories
  522. */
  523. public function getCategories($id, $sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0)
  524. {
  525. if (!DolibarrApiAccess::$user->rights->categorie->lire) {
  526. throw new RestException(401);
  527. }
  528. $categories = new Categorie($this->db);
  529. $result = $categories->getListForItem($id, 'product', $sortfield, $sortorder, $limit, $page);
  530. if ($result < 0) {
  531. throw new RestException(503, 'Error when retrieve category list : '.join(',', array_merge(array($categories->error), $categories->errors)));
  532. }
  533. return $result;
  534. }
  535. /**
  536. * Get prices per segment for a product
  537. *
  538. * @param int $id ID of product
  539. *
  540. * @return mixed
  541. *
  542. * @url GET {id}/selling_multiprices/per_segment
  543. */
  544. public function getCustomerPricesPerSegment($id)
  545. {
  546. global $conf;
  547. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  548. throw new RestException(401);
  549. }
  550. if (!getDolGlobalString('PRODUIT_MULTIPRICES')) {
  551. throw new RestException(400, 'API not available: this mode of pricing is not enabled by setup');
  552. }
  553. $result = $this->product->fetch($id);
  554. if (!$result) {
  555. throw new RestException(404, 'Product not found');
  556. }
  557. if ($result < 0) {
  558. throw new RestException(503, 'Error when retrieve prices list : '.join(',', array_merge(array($this->product->error), $this->product->errors)));
  559. }
  560. return array(
  561. 'multiprices'=>$this->product->multiprices,
  562. 'multiprices_inc_tax'=>$this->product->multiprices_ttc,
  563. 'multiprices_min'=>$this->product->multiprices_min,
  564. 'multiprices_min_inc_tax'=>$this->product->multiprices_min_ttc,
  565. 'multiprices_vat'=>$this->product->multiprices_tva_tx,
  566. 'multiprices_base_type'=>$this->product->multiprices_base_type,
  567. //'multiprices_default_vat_code'=>$this->product->multiprices_default_vat_code
  568. );
  569. }
  570. /**
  571. * Get prices per customer for a product
  572. *
  573. * @param int $id ID of product
  574. * @param string $thirdparty_id Thirdparty id to filter orders of (example '1') {@pattern /^[0-9,]*$/i}
  575. *
  576. * @return mixed
  577. *
  578. * @url GET {id}/selling_multiprices/per_customer
  579. */
  580. public function getCustomerPricesPerCustomer($id, $thirdparty_id = '')
  581. {
  582. global $conf;
  583. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  584. throw new RestException(401);
  585. }
  586. if (!getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
  587. throw new RestException(400, 'API not available: this mode of pricing is not enabled by setup');
  588. }
  589. $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : '';
  590. if ($socid > 0 && $socid != $thirdparty_id) {
  591. throw new RestException(401, 'Getting prices for all customers or for the customer ID '.$thirdparty_id.' is not allowed for login '.DolibarrApiAccess::$user->login);
  592. }
  593. $result = $this->product->fetch($id);
  594. if (!$result) {
  595. throw new RestException(404, 'Product not found');
  596. }
  597. if ($result > 0) {
  598. require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
  599. $prodcustprice = new ProductCustomerPrice($this->db);
  600. $filter = array();
  601. $filter['t.fk_product'] = $id;
  602. if ($thirdparty_id) {
  603. $filter['t.fk_soc'] = $thirdparty_id;
  604. }
  605. $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
  606. }
  607. if (empty($prodcustprice->lines)) {
  608. throw new RestException(404, 'Prices not found');
  609. }
  610. return $prodcustprice->lines;
  611. }
  612. /**
  613. * Get prices per quantity for a product
  614. *
  615. * @param int $id ID of product
  616. *
  617. * @return mixed
  618. *
  619. * @url GET {id}/selling_multiprices/per_quantity
  620. */
  621. public function getCustomerPricesPerQuantity($id)
  622. {
  623. global $conf;
  624. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  625. throw new RestException(401);
  626. }
  627. if (!getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
  628. throw new RestException(400, 'API not available: this mode of pricing is not enabled by setup');
  629. }
  630. $result = $this->product->fetch($id);
  631. if (!$result) {
  632. throw new RestException(404, 'Product not found');
  633. }
  634. if ($result < 0) {
  635. throw new RestException(503, 'Error when retrieve prices list : '.join(',', array_merge(array($this->product->error), $this->product->errors)));
  636. }
  637. return array(
  638. 'prices_by_qty'=>$this->product->prices_by_qty[0], // 1 if price by quantity was activated for the product
  639. 'prices_by_qty_list'=>$this->product->prices_by_qty_list[0]
  640. );
  641. }
  642. /**
  643. * Add/Update purchase prices for a product.
  644. *
  645. * @param int $id ID of Product
  646. * @param float $qty Min quantity for which price is valid
  647. * @param float $buyprice Purchase price for the quantity min
  648. * @param string $price_base_type HT or TTC
  649. * @param int $fourn_id Supplier ID
  650. * @param int $availability Product availability
  651. * @param string $ref_fourn Supplier ref
  652. * @param float $tva_tx New VAT Rate (For example 8.5. Should not be a string)
  653. * @param string $charges costs affering to product
  654. * @param float $remise_percent Discount regarding qty (percent)
  655. * @param float $remise Discount regarding qty (amount)
  656. * @param int $newnpr Set NPR or not
  657. * @param int $delivery_time_days Delay in days for delivery (max). May be '' if not defined.
  658. * @param string $supplier_reputation Reputation with this product to the defined supplier (empty, FAVORITE, DONOTORDER)
  659. * @param array $localtaxes_array Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function).
  660. * @param string $newdefaultvatcode Default vat code
  661. * @param float $multicurrency_buyprice Purchase price for the quantity min in currency
  662. * @param string $multicurrency_price_base_type HT or TTC in currency
  663. * @param float $multicurrency_tx Rate currency
  664. * @param string $multicurrency_code Currency code
  665. * @param string $desc_fourn Custom description for product_fourn_price
  666. * @param string $barcode Barcode
  667. * @param int $fk_barcode_type Barcode type
  668. * @return int
  669. *
  670. * @throws RestException 500 System error
  671. * @throws RestException 401
  672. *
  673. * @url POST {id}/purchase_prices
  674. */
  675. public function addPurchasePrice($id, $qty, $buyprice, $price_base_type, $fourn_id, $availability, $ref_fourn, $tva_tx, $charges = 0, $remise_percent = 0, $remise = 0, $newnpr = 0, $delivery_time_days = 0, $supplier_reputation = '', $localtaxes_array = array(), $newdefaultvatcode = '', $multicurrency_buyprice = 0, $multicurrency_price_base_type = 'HT', $multicurrency_tx = 1, $multicurrency_code = '', $desc_fourn = '', $barcode = '', $fk_barcode_type = null)
  676. {
  677. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  678. throw new RestException(401);
  679. }
  680. $result = $this->productsupplier->fetch($id);
  681. if (!$result) {
  682. throw new RestException(404, 'Product not found');
  683. }
  684. if (!DolibarrApi::_checkAccessToResource('product', $this->productsupplier->id)) {
  685. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  686. }
  687. $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : '';
  688. if ($socid > 0 && $socid != $fourn_id) {
  689. throw new RestException(401, 'Adding purchase price for the supplier ID '.$fourn_id.' is not allowed for login '.DolibarrApiAccess::$user->login);
  690. }
  691. $result = $this->productsupplier->add_fournisseur(DolibarrApiAccess::$user, $fourn_id, $ref_fourn, $qty);
  692. if ($result < 0) {
  693. throw new RestException(500, "Error adding supplier to product : ".$this->db->lasterror());
  694. }
  695. $fourn = new Fournisseur($this->db);
  696. $result = $fourn->fetch($fourn_id);
  697. if ($result <= 0) {
  698. throw new RestException(404, 'Supplier not found');
  699. }
  700. // Clean data
  701. $ref_fourn = sanitizeVal($ref_fourn, 'alphanohtml');
  702. $desc_fourn = sanitizeVal($desc_fourn, 'restricthtml');
  703. $barcode = sanitizeVal($barcode, 'alphanohtml');
  704. $result = $this->productsupplier->update_buyprice($qty, $buyprice, DolibarrApiAccess::$user, $price_base_type, $fourn, $availability, $ref_fourn, $tva_tx, $charges, $remise_percent, $remise, $newnpr, $delivery_time_days, $supplier_reputation, $localtaxes_array, $newdefaultvatcode, $multicurrency_buyprice, $multicurrency_price_base_type, $multicurrency_tx, $multicurrency_code, $desc_fourn, $barcode, $fk_barcode_type);
  705. if ($result <= 0) {
  706. throw new RestException(500, "Error updating buy price : ".$this->db->lasterror());
  707. }
  708. return (int) $this->productsupplier->product_fourn_price_id;
  709. }
  710. /**
  711. * Delete purchase price for a product
  712. *
  713. * @param int $id Product ID
  714. * @param int $priceid purchase price ID
  715. *
  716. * @url DELETE {id}/purchase_prices/{priceid}
  717. *
  718. * @return int
  719. *
  720. * @throws RestException 401
  721. * @throws RestException 404
  722. *
  723. */
  724. public function deletePurchasePrice($id, $priceid)
  725. {
  726. if (!DolibarrApiAccess::$user->rights->produit->supprimer) {
  727. throw new RestException(401);
  728. }
  729. $result = $this->productsupplier->fetch($id);
  730. if (!$result) {
  731. throw new RestException(404, 'Product not found');
  732. }
  733. if (!DolibarrApi::_checkAccessToResource('product', $this->productsupplier->id)) {
  734. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  735. }
  736. $resultsupplier = 0;
  737. if ($result > 0) {
  738. $resultsupplier = $this->productsupplier->remove_product_fournisseur_price($priceid);
  739. }
  740. return $resultsupplier;
  741. }
  742. /**
  743. * Get a list of all purchase prices of products
  744. *
  745. * @param string $sortfield Sort field
  746. * @param string $sortorder Sort order
  747. * @param int $limit Limit for list
  748. * @param int $page Page number
  749. * @param int $mode Use this param to filter list (0 for all, 1 for only product, 2 for only service)
  750. * @param int $category Use this param to filter list by category of product
  751. * @param int $supplier Use this param to filter list by supplier
  752. * @param string $sqlfilters Other criteria to filter answers separated by a comma. Syntax example "(t.tobuy:=:0) and (t.tosell:=:1)"
  753. * @return array Array of product objects
  754. *
  755. * @url GET purchase_prices
  756. */
  757. public function getSupplierProducts($sortfield = "t.ref", $sortorder = 'ASC', $limit = 100, $page = 0, $mode = 0, $category = 0, $supplier = 0, $sqlfilters = '')
  758. {
  759. global $db, $conf;
  760. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  761. throw new RestException(401);
  762. }
  763. $obj_ret = array();
  764. // Force id of company for external users
  765. $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : '';
  766. if ($socid > 0) {
  767. if ($supplier != $socid || empty($supplier)) {
  768. throw new RestException(401, 'As an external user, you can request only for your supplier id = '.$socid);
  769. }
  770. }
  771. $sql = "SELECT t.rowid, t.ref, t.ref_ext";
  772. $sql .= " FROM ".MAIN_DB_PREFIX."product AS t LEFT JOIN ".MAIN_DB_PREFIX."product_extrafields AS ef ON (ef.fk_object = t.rowid)"; // Modification VMR Global Solutions to include extrafields as search parameters in the API GET call, so we will be able to filter on extrafields
  773. if ($category > 0) {
  774. $sql .= ", ".$this->db->prefix()."categorie_product as c";
  775. }
  776. $sql .= ", ".$this->db->prefix()."product_fournisseur_price as s";
  777. $sql .= ' WHERE t.entity IN ('.getEntity('product').')';
  778. if ($supplier > 0) {
  779. $sql .= " AND s.fk_soc = ".((int) $supplier);
  780. }
  781. if ($socid > 0) { // if external user
  782. $sql .= " AND s.fk_soc = ".((int) $socid);
  783. }
  784. $sql .= " AND s.fk_product = t.rowid";
  785. // Select products of given category
  786. if ($category > 0) {
  787. $sql .= " AND c.fk_categorie = ".((int) $category);
  788. $sql .= " AND c.fk_product = t.rowid";
  789. }
  790. if ($mode == 1) {
  791. // Show only products
  792. $sql .= " AND t.fk_product_type = 0";
  793. } elseif ($mode == 2) {
  794. // Show only services
  795. $sql .= " AND t.fk_product_type = 1";
  796. }
  797. // Add sql filters
  798. if ($sqlfilters) {
  799. $errormessage = '';
  800. $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
  801. if ($errormessage) {
  802. throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage);
  803. }
  804. }
  805. $sql .= $this->db->order($sortfield, $sortorder);
  806. if ($limit) {
  807. if ($page < 0) {
  808. $page = 0;
  809. }
  810. $offset = $limit * $page;
  811. $sql .= $this->db->plimit($limit + 1, $offset);
  812. }
  813. $result = $this->db->query($sql);
  814. if ($result) {
  815. $num = $this->db->num_rows($result);
  816. $min = min($num, ($limit <= 0 ? $num : $limit));
  817. $i = 0;
  818. while ($i < $min) {
  819. $obj = $this->db->fetch_object($result);
  820. $product_fourn = new ProductFournisseur($this->db);
  821. $product_fourn_list = $product_fourn->list_product_fournisseur_price($obj->rowid, '', '', 0, 0);
  822. foreach ($product_fourn_list as $tmpobj) {
  823. $this->_cleanObjectDatas($tmpobj);
  824. }
  825. //var_dump($product_fourn_list->db);exit;
  826. $obj_ret[$obj->rowid] = $product_fourn_list;
  827. $i++;
  828. }
  829. } else {
  830. throw new RestException(503, 'Error when retrieve product list : '.$this->db->lasterror());
  831. }
  832. return $obj_ret;
  833. }
  834. /**
  835. * Get purchase prices for a product
  836. *
  837. * Return an array with product information.
  838. * TODO implement getting a product by ref or by $ref_ext
  839. *
  840. * @param int $id ID of product
  841. * @param string $ref Ref of element
  842. * @param string $ref_ext Ref ext of element
  843. * @param string $barcode Barcode of element
  844. * @return array|mixed Data without useless information
  845. *
  846. * @url GET {id}/purchase_prices
  847. *
  848. * @throws RestException 401
  849. * @throws RestException 403
  850. * @throws RestException 404
  851. *
  852. */
  853. public function getPurchasePrices($id, $ref = '', $ref_ext = '', $barcode = '')
  854. {
  855. if (empty($id) && empty($ref) && empty($ref_ext) && empty($barcode)) {
  856. throw new RestException(400, 'bad value for parameter id, ref, ref_ext or barcode');
  857. }
  858. $id = (empty($id) ? 0 : $id);
  859. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  860. throw new RestException(403);
  861. }
  862. $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : '';
  863. $result = $this->product->fetch($id, $ref, $ref_ext, $barcode);
  864. if (!$result) {
  865. throw new RestException(404, 'Product not found');
  866. }
  867. if (!DolibarrApi::_checkAccessToResource('product', $this->product->id)) {
  868. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  869. }
  870. $product_fourn_list = array();
  871. if ($result) {
  872. $product_fourn = new ProductFournisseur($this->db);
  873. $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->product->id, '', '', 0, 0, ($socid > 0 ? $socid : 0));
  874. }
  875. foreach ($product_fourn_list as $tmpobj) {
  876. $this->_cleanObjectDatas($tmpobj);
  877. }
  878. return $this->_cleanObjectDatas($product_fourn_list);
  879. }
  880. /**
  881. * Get attributes.
  882. *
  883. * @param string $sortfield Sort field
  884. * @param string $sortorder Sort order
  885. * @param int $limit Limit for list
  886. * @param int $page Page number
  887. * @param string $sqlfilters Other criteria to filter answers separated by a comma. Syntax example "(t.ref:like:color)"
  888. * @param string $properties Restrict the data returned to theses properties. Ignored if empty. Comma separated list of properties names
  889. * @return array
  890. *
  891. * @throws RestException 401
  892. * @throws RestException 404
  893. * @throws RestException 503
  894. *
  895. * @url GET attributes
  896. */
  897. public function getAttributes($sortfield = "t.ref", $sortorder = 'ASC', $limit = 100, $page = 0, $sqlfilters = '', $properties = '')
  898. {
  899. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  900. throw new RestException(401);
  901. }
  902. $sql = "SELECT t.rowid, t.ref, t.ref_ext, t.label, t.position, t.entity";
  903. $sql .= " FROM ".$this->db->prefix()."product_attribute as t";
  904. $sql .= ' WHERE t.entity IN ('.getEntity('product').')';
  905. // Add sql filters
  906. if ($sqlfilters) {
  907. $errormessage = '';
  908. $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
  909. if ($errormessage) {
  910. throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage);
  911. }
  912. }
  913. $sql .= $this->db->order($sortfield, $sortorder);
  914. if ($limit) {
  915. if ($page < 0) {
  916. $page = 0;
  917. }
  918. $offset = $limit * $page;
  919. $sql .= $this->db->plimit($limit, $offset);
  920. }
  921. $resql = $this->db->query($sql);
  922. if (!$resql) {
  923. throw new RestException(503, 'Error when retrieving product attribute list : '.$this->db->lasterror());
  924. }
  925. $return = array();
  926. while ($obj = $this->db->fetch_object($resql)) {
  927. $tmp = new ProductAttribute($this->db);
  928. $tmp->id = $obj->rowid;
  929. $tmp->ref = $obj->ref;
  930. $tmp->ref_ext = $obj->ref_ext;
  931. $tmp->label = $obj->label;
  932. $tmp->position = $obj->position;
  933. $tmp->entity = $obj->entity;
  934. $return[] = $this->_filterObjectProperties($this->_cleanObjectDatas($tmp), $properties);
  935. }
  936. return $return;
  937. }
  938. /**
  939. * Get attribute by ID.
  940. *
  941. * @param int $id ID of Attribute
  942. * @return Object Object with cleaned properties
  943. *
  944. * @throws RestException 401
  945. * @throws RestException 404
  946. *
  947. * @url GET attributes/{id}
  948. */
  949. public function getAttributeById($id)
  950. {
  951. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  952. throw new RestException(401);
  953. }
  954. $prodattr = new ProductAttribute($this->db);
  955. $result = $prodattr->fetch((int) $id);
  956. if ($result < 0) {
  957. throw new RestException(404, "Product attribute not found");
  958. }
  959. $fields = ["id", "ref", "ref_ext", "label", "position", "entity"];
  960. foreach ($prodattr as $field => $value) {
  961. if (!in_array($field, $fields)) {
  962. unset($prodattr->{$field});
  963. }
  964. }
  965. $sql = "SELECT COUNT(*) as nb FROM ".$this->db->prefix()."product_attribute_combination2val as pac2v";
  966. $sql .= " JOIN ".$this->db->prefix()."product_attribute_combination as pac ON pac2v.fk_prod_combination = pac.rowid";
  967. $sql .= " WHERE pac2v.fk_prod_attr = ".((int) $prodattr->id)." AND pac.entity IN (".getEntity('product').")";
  968. $resql = $this->db->query($sql);
  969. $obj = $this->db->fetch_object($resql);
  970. $prodattr->is_used_by_products = (int) $obj->nb;
  971. return $this->_cleanObjectDatas($prodattr);
  972. }
  973. /**
  974. * Get attributes by ref.
  975. *
  976. * @param string $ref Reference of Attribute
  977. * @return array
  978. *
  979. * @throws RestException 401
  980. * @throws RestException 404
  981. *
  982. * @url GET attributes/ref/{ref}
  983. */
  984. public function getAttributesByRef($ref)
  985. {
  986. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  987. throw new RestException(401);
  988. }
  989. $ref = trim($ref);
  990. $sql = "SELECT rowid, ref, ref_ext, label, position, entity FROM ".$this->db->prefix()."product_attribute WHERE ref LIKE '".$this->db->escape($ref)."' AND entity IN (".getEntity('product').")";
  991. $query = $this->db->query($sql);
  992. if (!$this->db->num_rows($query)) {
  993. throw new RestException(404);
  994. }
  995. $result = $this->db->fetch_object($query);
  996. $attr = array();
  997. $attr['id'] = $result->rowid;
  998. $attr['ref'] = $result->ref;
  999. $attr['ref_ext'] = $result->ref_ext;
  1000. $attr['label'] = $result->label;
  1001. $attr['rang'] = $result->position;
  1002. $attr['position'] = $result->position;
  1003. $attr['entity'] = $result->entity;
  1004. $sql = "SELECT COUNT(*) as nb FROM ".$this->db->prefix()."product_attribute_combination2val as pac2v";
  1005. $sql .= " JOIN ".$this->db->prefix()."product_attribute_combination as pac ON pac2v.fk_prod_combination = pac.rowid";
  1006. $sql .= " WHERE pac2v.fk_prod_attr = ".((int) $result->rowid)." AND pac.entity IN (".getEntity('product').")";
  1007. $resql = $this->db->query($sql);
  1008. $obj = $this->db->fetch_object($resql);
  1009. $attr["is_used_by_products"] = (int) $obj->nb;
  1010. return $attr;
  1011. }
  1012. /**
  1013. * Get attributes by ref_ext.
  1014. *
  1015. * @param string $ref_ext External reference of Attribute
  1016. * @return array
  1017. *
  1018. * @throws RestException 500 System error
  1019. * @throws RestException 401
  1020. *
  1021. * @url GET attributes/ref_ext/{ref_ext}
  1022. */
  1023. public function getAttributesByRefExt($ref_ext)
  1024. {
  1025. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  1026. throw new RestException(401);
  1027. }
  1028. $ref_ext = trim($ref_ext);
  1029. $sql = "SELECT rowid, ref, ref_ext, label, position, entity FROM ".$this->db->prefix()."product_attribute WHERE ref_ext LIKE '".$this->db->escape($ref_ext)."' AND entity IN (".getEntity('product').")";
  1030. $query = $this->db->query($sql);
  1031. if (!$this->db->num_rows($query)) {
  1032. throw new RestException(404);
  1033. }
  1034. $result = $this->db->fetch_object($query);
  1035. $attr = array();
  1036. $attr['id'] = $result->rowid;
  1037. $attr['ref'] = $result->ref;
  1038. $attr['ref_ext'] = $result->ref_ext;
  1039. $attr['label'] = $result->label;
  1040. $attr['rang'] = $result->position;
  1041. $attr['position'] = $result->position;
  1042. $attr['entity'] = $result->entity;
  1043. $sql = "SELECT COUNT(*) as nb FROM ".$this->db->prefix()."product_attribute_combination2val as pac2v";
  1044. $sql .= " JOIN ".$this->db->prefix()."product_attribute_combination as pac ON pac2v.fk_prod_combination = pac.rowid";
  1045. $sql .= " WHERE pac2v.fk_prod_attr = ".((int) $result->rowid)." AND pac.entity IN (".getEntity('product').")";
  1046. $resql = $this->db->query($sql);
  1047. $obj = $this->db->fetch_object($resql);
  1048. $attr["is_used_by_products"] = (int) $obj->nb;
  1049. return $attr;
  1050. }
  1051. /**
  1052. * Add attributes.
  1053. *
  1054. * @param string $ref Reference of Attribute
  1055. * @param string $label Label of Attribute
  1056. * @param string $ref_ext Reference of Attribute
  1057. * @return int
  1058. *
  1059. * @throws RestException 500 System error
  1060. * @throws RestException 401
  1061. *
  1062. * @url POST attributes
  1063. */
  1064. public function addAttributes($ref, $label, $ref_ext = '')
  1065. {
  1066. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  1067. throw new RestException(401);
  1068. }
  1069. $prodattr = new ProductAttribute($this->db);
  1070. $prodattr->label = $label;
  1071. $prodattr->ref = $ref;
  1072. $prodattr->ref_ext = $ref_ext;
  1073. $resid = $prodattr->create(DolibarrApiAccess::$user);
  1074. if ($resid <= 0) {
  1075. throw new RestException(500, "Error creating new attribute");
  1076. }
  1077. return $resid;
  1078. }
  1079. /**
  1080. * Update attributes by id.
  1081. *
  1082. * @param int $id ID of Attribute
  1083. * @param array $request_data Datas
  1084. * @return Object Object with cleaned properties
  1085. *
  1086. * @throws RestException
  1087. * @throws RestException 401
  1088. * @throws RestException 404
  1089. *
  1090. * @url PUT attributes/{id}
  1091. */
  1092. public function putAttributes($id, $request_data = null)
  1093. {
  1094. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  1095. throw new RestException(401);
  1096. }
  1097. $prodattr = new ProductAttribute($this->db);
  1098. $result = $prodattr->fetch((int) $id);
  1099. if ($result == 0) {
  1100. throw new RestException(404, 'Attribute not found');
  1101. } elseif ($result < 0) {
  1102. throw new RestException(500, "Error fetching attribute");
  1103. }
  1104. foreach ($request_data as $field => $value) {
  1105. if ($field == 'rowid') {
  1106. continue;
  1107. }
  1108. if ($field === 'caller') {
  1109. // Add a mention of caller so on trigger called after action, we can filter to avoid a loop if we try to sync back again whith the caller
  1110. $prodattr->context['caller'] = $request_data['caller'];
  1111. continue;
  1112. }
  1113. $prodattr->$field = $value;
  1114. }
  1115. if ($prodattr->update(DolibarrApiAccess::$user) > 0) {
  1116. $result = $prodattr->fetch((int) $id);
  1117. if ($result == 0) {
  1118. throw new RestException(404, 'Attribute not found');
  1119. } elseif ($result < 0) {
  1120. throw new RestException(500, "Error fetching attribute");
  1121. } else {
  1122. return $this->_cleanObjectDatas($prodattr);
  1123. }
  1124. }
  1125. throw new RestException(500, "Error updating attribute");
  1126. }
  1127. /**
  1128. * Delete attributes by id.
  1129. *
  1130. * @param int $id ID of Attribute
  1131. * @return int Result of deletion
  1132. *
  1133. * @throws RestException 500 System error
  1134. * @throws RestException 401
  1135. *
  1136. * @url DELETE attributes/{id}
  1137. */
  1138. public function deleteAttributes($id)
  1139. {
  1140. if (!DolibarrApiAccess::$user->rights->produit->supprimer) {
  1141. throw new RestException(401);
  1142. }
  1143. $prodattr = new ProductAttribute($this->db);
  1144. $prodattr->id = (int) $id;
  1145. $result = $prodattr->delete(DolibarrApiAccess::$user);
  1146. if ($result <= 0) {
  1147. throw new RestException(500, "Error deleting attribute");
  1148. }
  1149. return $result;
  1150. }
  1151. /**
  1152. * Get attribute value by id.
  1153. *
  1154. * @param int $id ID of Attribute value
  1155. * @return array
  1156. *
  1157. * @throws RestException 500 System error
  1158. * @throws RestException 401
  1159. *
  1160. * @url GET attributes/values/{id}
  1161. */
  1162. public function getAttributeValueById($id)
  1163. {
  1164. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  1165. throw new RestException(401);
  1166. }
  1167. $sql = "SELECT rowid, fk_product_attribute, ref, value FROM ".$this->db->prefix()."product_attribute_value WHERE rowid = ".(int) $id." AND entity IN (".getEntity('product').")";
  1168. $query = $this->db->query($sql);
  1169. if (!$query) {
  1170. throw new RestException(401);
  1171. }
  1172. if (!$this->db->num_rows($query)) {
  1173. throw new RestException(404, 'Attribute value not found');
  1174. }
  1175. $result = $this->db->fetch_object($query);
  1176. $attrval = array();
  1177. $attrval['id'] = $result->rowid;
  1178. $attrval['fk_product_attribute'] = $result->fk_product_attribute;
  1179. $attrval['ref'] = $result->ref;
  1180. $attrval['value'] = $result->value;
  1181. return $attrval;
  1182. }
  1183. /**
  1184. * Get attribute value by ref.
  1185. *
  1186. * @param int $id ID of Attribute value
  1187. * @param string $ref Ref of Attribute value
  1188. * @return array
  1189. *
  1190. * @throws RestException 500 System error
  1191. * @throws RestException 401
  1192. *
  1193. * @url GET attributes/{id}/values/ref/{ref}
  1194. */
  1195. public function getAttributeValueByRef($id, $ref)
  1196. {
  1197. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  1198. throw new RestException(401);
  1199. }
  1200. $ref = trim($ref);
  1201. $sql = "SELECT rowid, fk_product_attribute, ref, value FROM ".$this->db->prefix()."product_attribute_value";
  1202. $sql .= " WHERE ref LIKE '".$this->db->escape($ref)."' AND fk_product_attribute = ".((int) $id)." AND entity IN (".getEntity('product').")";
  1203. $query = $this->db->query($sql);
  1204. if (!$query) {
  1205. throw new RestException(401);
  1206. }
  1207. if (!$this->db->num_rows($query)) {
  1208. throw new RestException(404, 'Attribute value not found');
  1209. }
  1210. $result = $this->db->fetch_object($query);
  1211. $attrval = array();
  1212. $attrval['id'] = $result->rowid;
  1213. $attrval['fk_product_attribute'] = $result->fk_product_attribute;
  1214. $attrval['ref'] = $result->ref;
  1215. $attrval['value'] = $result->value;
  1216. return $attrval;
  1217. }
  1218. /**
  1219. * Delete attribute value by ref.
  1220. *
  1221. * @param int $id ID of Attribute
  1222. * @param string $ref Ref of Attribute value
  1223. * @return int
  1224. *
  1225. * @throws RestException 401
  1226. *
  1227. * @url DELETE attributes/{id}/values/ref/{ref}
  1228. */
  1229. public function deleteAttributeValueByRef($id, $ref)
  1230. {
  1231. if (!DolibarrApiAccess::$user->rights->produit->supprimer) {
  1232. throw new RestException(401);
  1233. }
  1234. $ref = trim($ref);
  1235. $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_value";
  1236. $sql .= " WHERE ref LIKE '".$this->db->escape($ref)."' AND fk_product_attribute = ".((int) $id)." AND entity IN (".getEntity('product').")";
  1237. $query = $this->db->query($sql);
  1238. if (!$query) {
  1239. throw new RestException(401);
  1240. }
  1241. if (!$this->db->num_rows($query)) {
  1242. throw new RestException(404, 'Attribute value not found');
  1243. }
  1244. $result = $this->db->fetch_object($query);
  1245. $attrval = new ProductAttributeValue($this->db);
  1246. $attrval->id = $result->rowid;
  1247. $result = $attrval->delete(DolibarrApiAccess::$user);
  1248. if ($result > 0) {
  1249. return 1;
  1250. }
  1251. throw new RestException(500, "Error deleting attribute value");
  1252. }
  1253. /**
  1254. * Get all values for an attribute id.
  1255. *
  1256. * @param int $id ID of an Attribute
  1257. * @return array
  1258. *
  1259. * @throws RestException 401
  1260. * @throws RestException 500 System error
  1261. *
  1262. * @url GET attributes/{id}/values
  1263. */
  1264. public function getAttributeValues($id)
  1265. {
  1266. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  1267. throw new RestException(401);
  1268. }
  1269. $objectval = new ProductAttributeValue($this->db);
  1270. $return = $objectval->fetchAllByProductAttribute((int) $id);
  1271. if (count($return) == 0) {
  1272. throw new RestException(404, 'Attribute values not found');
  1273. }
  1274. foreach ($return as $key => $val) {
  1275. $return[$key] = $this->_cleanObjectDatas($return[$key]);
  1276. }
  1277. return $return;
  1278. }
  1279. /**
  1280. * Get all values for an attribute ref.
  1281. *
  1282. * @param string $ref Ref of an Attribute
  1283. * @return array
  1284. *
  1285. * @throws RestException 401
  1286. *
  1287. * @url GET attributes/ref/{ref}/values
  1288. */
  1289. public function getAttributeValuesByRef($ref)
  1290. {
  1291. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  1292. throw new RestException(401);
  1293. }
  1294. $ref = trim($ref);
  1295. $return = array();
  1296. $sql = "SELECT ";
  1297. $sql .= "v.fk_product_attribute, v.rowid, v.ref, v.value FROM ".$this->db->prefix()."product_attribute_value as v";
  1298. $sql .= " WHERE v.fk_product_attribute IN (SELECT rowid FROM ".$this->db->prefix()."product_attribute WHERE ref LIKE '".$this->db->escape($ref)."')";
  1299. $resql = $this->db->query($sql);
  1300. while ($result = $this->db->fetch_object($resql)) {
  1301. $tmp = new ProductAttributeValue($this->db);
  1302. $tmp->fk_product_attribute = $result->fk_product_attribute;
  1303. $tmp->id = $result->rowid;
  1304. $tmp->ref = $result->ref;
  1305. $tmp->value = $result->value;
  1306. $return[] = $this->_cleanObjectDatas($tmp);
  1307. }
  1308. return $return;
  1309. }
  1310. /**
  1311. * Add attribute value.
  1312. *
  1313. * @param int $id ID of Attribute
  1314. * @param string $ref Reference of Attribute value
  1315. * @param string $value Value of Attribute value
  1316. * @return int
  1317. *
  1318. * @throws RestException 500 System error
  1319. * @throws RestException 401
  1320. *
  1321. * @url POST attributes/{id}/values
  1322. */
  1323. public function addAttributeValue($id, $ref, $value)
  1324. {
  1325. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  1326. throw new RestException(401);
  1327. }
  1328. if (empty($ref) || empty($value)) {
  1329. throw new RestException(401);
  1330. }
  1331. $objectval = new ProductAttributeValue($this->db);
  1332. $objectval->fk_product_attribute = ((int) $id);
  1333. $objectval->ref = $ref;
  1334. $objectval->value = $value;
  1335. if ($objectval->create(DolibarrApiAccess::$user) > 0) {
  1336. return $objectval->id;
  1337. }
  1338. throw new RestException(500, "Error creating new attribute value");
  1339. }
  1340. /**
  1341. * Update attribute value.
  1342. *
  1343. * @param int $id ID of Attribute
  1344. * @param array $request_data Datas
  1345. * @return Object Object with cleaned properties
  1346. *
  1347. * @throws RestException 401
  1348. * @throws RestException 500 System error
  1349. *
  1350. * @url PUT attributes/values/{id}
  1351. */
  1352. public function putAttributeValue($id, $request_data)
  1353. {
  1354. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  1355. throw new RestException(401);
  1356. }
  1357. $objectval = new ProductAttributeValue($this->db);
  1358. $result = $objectval->fetch((int) $id);
  1359. if ($result == 0) {
  1360. throw new RestException(404, 'Attribute value not found');
  1361. } elseif ($result < 0) {
  1362. throw new RestException(500, "Error fetching attribute value");
  1363. }
  1364. foreach ($request_data as $field => $value) {
  1365. if ($field == 'rowid') {
  1366. continue;
  1367. }
  1368. if ($field === 'caller') {
  1369. // Add a mention of caller so on trigger called after action, we can filter to avoid a loop if we try to sync back again whith the caller
  1370. $objectval->context['caller'] = $request_data['caller'];
  1371. continue;
  1372. }
  1373. $objectval->$field = $value;
  1374. }
  1375. if ($objectval->update(DolibarrApiAccess::$user) > 0) {
  1376. $result = $objectval->fetch((int) $id);
  1377. if ($result == 0) {
  1378. throw new RestException(404, 'Attribute not found');
  1379. } elseif ($result < 0) {
  1380. throw new RestException(500, "Error fetching attribute");
  1381. } else {
  1382. return $this->_cleanObjectDatas($objectval);
  1383. }
  1384. }
  1385. throw new RestException(500, "Error updating attribute");
  1386. }
  1387. /**
  1388. * Delete attribute value by id.
  1389. *
  1390. * @param int $id ID of Attribute value
  1391. * @return int
  1392. *
  1393. * @throws RestException 500 System error
  1394. * @throws RestException 401
  1395. *
  1396. * @url DELETE attributes/values/{id}
  1397. */
  1398. public function deleteAttributeValueById($id)
  1399. {
  1400. if (!DolibarrApiAccess::$user->rights->produit->supprimer) {
  1401. throw new RestException(401);
  1402. }
  1403. $objectval = new ProductAttributeValue($this->db);
  1404. $objectval->id = (int) $id;
  1405. if ($objectval->delete(DolibarrApiAccess::$user) > 0) {
  1406. return 1;
  1407. }
  1408. throw new RestException(500, "Error deleting attribute value");
  1409. }
  1410. /**
  1411. * Get product variants.
  1412. *
  1413. * @param int $id ID of Product
  1414. * @param int $includestock Default value 0. If parameter is set to 1 the response will contain stock data of each variant
  1415. * @return array
  1416. *
  1417. * @throws RestException 500 System error
  1418. * @throws RestException 401
  1419. *
  1420. * @url GET {id}/variants
  1421. */
  1422. public function getVariants($id, $includestock = 0)
  1423. {
  1424. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  1425. throw new RestException(401);
  1426. }
  1427. $prodcomb = new ProductCombination($this->db);
  1428. $combinations = $prodcomb->fetchAllByFkProductParent((int) $id);
  1429. foreach ($combinations as $key => $combination) {
  1430. $prodc2vp = new ProductCombination2ValuePair($this->db);
  1431. $combinations[$key]->attributes = $prodc2vp->fetchByFkCombination((int) $combination->id);
  1432. $combinations[$key] = $this->_cleanObjectDatas($combinations[$key]);
  1433. if (!empty($includestock) && DolibarrApiAccess::$user->rights->stock->lire) {
  1434. $productModel = new Product($this->db);
  1435. $productModel->fetch((int) $combination->fk_product_child);
  1436. $productModel->load_stock($includestock);
  1437. $combinations[$key]->stock_warehouse = $this->_cleanObjectDatas($productModel)->stock_warehouse;
  1438. }
  1439. }
  1440. return $combinations;
  1441. }
  1442. /**
  1443. * Get product variants by Product ref.
  1444. *
  1445. * @param string $ref Ref of Product
  1446. * @return array
  1447. *
  1448. * @throws RestException 500 System error
  1449. * @throws RestException 401
  1450. *
  1451. * @url GET ref/{ref}/variants
  1452. */
  1453. public function getVariantsByProdRef($ref)
  1454. {
  1455. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  1456. throw new RestException(401);
  1457. }
  1458. $result = $this->product->fetch('', $ref);
  1459. if (!$result) {
  1460. throw new RestException(404, 'Product not found');
  1461. }
  1462. $prodcomb = new ProductCombination($this->db);
  1463. $combinations = $prodcomb->fetchAllByFkProductParent((int) $this->product->id);
  1464. foreach ($combinations as $key => $combination) {
  1465. $prodc2vp = new ProductCombination2ValuePair($this->db);
  1466. $combinations[$key]->attributes = $prodc2vp->fetchByFkCombination((int) $combination->id);
  1467. $combinations[$key] = $this->_cleanObjectDatas($combinations[$key]);
  1468. }
  1469. return $combinations;
  1470. }
  1471. /**
  1472. * Add variant.
  1473. *
  1474. * "features" is a list of attributes pairs id_attribute=>id_value. Example: array(id_color=>id_Blue, id_size=>id_small, id_option=>id_val_a, ...)
  1475. *
  1476. * @param int $id ID of Product
  1477. * @param float $weight_impact Weight impact of variant
  1478. * @param float $price_impact Price impact of variant
  1479. * @param bool $price_impact_is_percent Price impact in percent (true or false)
  1480. * @param array $features List of attributes pairs id_attribute->id_value. Example: array(id_color=>id_Blue, id_size=>id_small, id_option=>id_val_a, ...)
  1481. * @param string $reference Customized reference of variant
  1482. * @param string $ref_ext External reference of variant
  1483. * @return int
  1484. *
  1485. * @throws RestException 500 System error
  1486. * @throws RestException 401
  1487. * @throws RestException 404
  1488. *
  1489. * @url POST {id}/variants
  1490. */
  1491. public function addVariant($id, $weight_impact, $price_impact, $price_impact_is_percent, $features, $reference = '', $ref_ext = '')
  1492. {
  1493. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  1494. throw new RestException(401);
  1495. }
  1496. if (empty($id) || empty($features) || !is_array($features)) {
  1497. throw new RestException(401);
  1498. }
  1499. $weight_impact = price2num($weight_impact);
  1500. $price_impact = price2num($price_impact);
  1501. $prodattr = new ProductAttribute($this->db);
  1502. $prodattr_val = new ProductAttributeValue($this->db);
  1503. foreach ($features as $id_attr => $id_value) {
  1504. if ($prodattr->fetch((int) $id_attr) < 0) {
  1505. throw new RestException(401);
  1506. }
  1507. if ($prodattr_val->fetch((int) $id_value) < 0) {
  1508. throw new RestException(401);
  1509. }
  1510. }
  1511. $result = $this->product->fetch((int) $id);
  1512. if (!$result) {
  1513. throw new RestException(404, 'Product not found');
  1514. }
  1515. $prodcomb = new ProductCombination($this->db);
  1516. $result = $prodcomb->createProductCombination(DolibarrApiAccess::$user, $this->product, $features, array(), $price_impact_is_percent, $price_impact, $weight_impact, $reference, $ref_ext);
  1517. if ($result > 0) {
  1518. return $result;
  1519. } else {
  1520. throw new RestException(500, "Error creating new product variant");
  1521. }
  1522. }
  1523. /**
  1524. * Add variant by product ref.
  1525. *
  1526. * "features" is a list of attributes pairs id_attribute=>id_value. Example: array(id_color=>id_Blue, id_size=>id_small, id_option=>id_val_a, ...)
  1527. *
  1528. * @param string $ref Ref of Product
  1529. * @param float $weight_impact Weight impact of variant
  1530. * @param float $price_impact Price impact of variant
  1531. * @param bool $price_impact_is_percent Price impact in percent (true or false)
  1532. * @param array $features List of attributes pairs id_attribute->id_value. Example: array(id_color=>id_Blue, id_size=>id_small, id_option=>id_val_a, ...)
  1533. * @return int
  1534. *
  1535. * @throws RestException 500 System error
  1536. * @throws RestException 401
  1537. * @throws RestException 404
  1538. *
  1539. * @url POST ref/{ref}/variants
  1540. */
  1541. public function addVariantByProductRef($ref, $weight_impact, $price_impact, $price_impact_is_percent, $features)
  1542. {
  1543. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  1544. throw new RestException(401);
  1545. }
  1546. if (empty($ref) || empty($features) || !is_array($features)) {
  1547. throw new RestException(401);
  1548. }
  1549. $weight_impact = price2num($weight_impact);
  1550. $price_impact = price2num($price_impact);
  1551. $prodattr = new ProductAttribute($this->db);
  1552. $prodattr_val = new ProductAttributeValue($this->db);
  1553. foreach ($features as $id_attr => $id_value) {
  1554. if ($prodattr->fetch((int) $id_attr) < 0) {
  1555. throw new RestException(404);
  1556. }
  1557. if ($prodattr_val->fetch((int) $id_value) < 0) {
  1558. throw new RestException(404);
  1559. }
  1560. }
  1561. $result = $this->product->fetch('', trim($ref));
  1562. if (!$result) {
  1563. throw new RestException(404, 'Product not found');
  1564. }
  1565. $prodcomb = new ProductCombination($this->db);
  1566. if (!$prodcomb->fetchByProductCombination2ValuePairs($this->product->id, $features)) {
  1567. $result = $prodcomb->createProductCombination(DolibarrApiAccess::$user, $this->product, $features, array(), $price_impact_is_percent, $price_impact, $weight_impact);
  1568. if ($result > 0) {
  1569. return $result;
  1570. } else {
  1571. throw new RestException(500, "Error creating new product variant");
  1572. }
  1573. } else {
  1574. return $prodcomb->id;
  1575. }
  1576. }
  1577. /**
  1578. * Put product variants.
  1579. *
  1580. * @param int $id ID of Variant
  1581. * @param array $request_data Datas
  1582. * @return int
  1583. *
  1584. * @throws RestException 500 System error
  1585. * @throws RestException 401
  1586. *
  1587. * @url PUT variants/{id}
  1588. */
  1589. public function putVariant($id, $request_data = null)
  1590. {
  1591. if (!DolibarrApiAccess::$user->rights->produit->creer) {
  1592. throw new RestException(401);
  1593. }
  1594. $prodcomb = new ProductCombination($this->db);
  1595. $prodcomb->fetch((int) $id);
  1596. foreach ($request_data as $field => $value) {
  1597. if ($field == 'rowid') {
  1598. continue;
  1599. }
  1600. if ($field === 'caller') {
  1601. // Add a mention of caller so on trigger called after action, we can filter to avoid a loop if we try to sync back again whith the caller
  1602. $prodcomb->context['caller'] = $request_data['caller'];
  1603. continue;
  1604. }
  1605. $prodcomb->$field = $value;
  1606. }
  1607. $result = $prodcomb->update(DolibarrApiAccess::$user);
  1608. if ($result > 0) {
  1609. return 1;
  1610. }
  1611. throw new RestException(500, "Error editing variant");
  1612. }
  1613. /**
  1614. * Delete product variants.
  1615. *
  1616. * @param int $id ID of Variant
  1617. * @return int Result of deletion
  1618. *
  1619. * @throws RestException 500 System error
  1620. * @throws RestException 401
  1621. *
  1622. * @url DELETE variants/{id}
  1623. */
  1624. public function deleteVariant($id)
  1625. {
  1626. if (!DolibarrApiAccess::$user->rights->produit->supprimer) {
  1627. throw new RestException(401);
  1628. }
  1629. $prodcomb = new ProductCombination($this->db);
  1630. $prodcomb->id = (int) $id;
  1631. $result = $prodcomb->delete(DolibarrApiAccess::$user);
  1632. if ($result <= 0) {
  1633. throw new RestException(500, "Error deleting variant");
  1634. }
  1635. return $result;
  1636. }
  1637. /**
  1638. * Get stock data for the product id given.
  1639. * Optionaly with $selected_warehouse_id parameter user can get stock of specific warehouse
  1640. *
  1641. * @param int $id ID of Product
  1642. * @param int $selected_warehouse_id ID of warehouse
  1643. * @return array
  1644. *
  1645. * @throws RestException 500 System error
  1646. * @throws RestException 401
  1647. * @throws RestException 404
  1648. *
  1649. * @url GET {id}/stock
  1650. */
  1651. public function getStock($id, $selected_warehouse_id = null)
  1652. {
  1653. if (!DolibarrApiAccess::$user->rights->produit->lire || !DolibarrApiAccess::$user->rights->stock->lire) {
  1654. throw new RestException(401);
  1655. }
  1656. if (!DolibarrApi::_checkAccessToResource('product', $id)) {
  1657. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  1658. }
  1659. $product_model = new Product($this->db);
  1660. $product_model->fetch($id);
  1661. $product_model->load_stock();
  1662. $stockData = $this->_cleanObjectDatas($product_model)->stock_warehouse;
  1663. if ($selected_warehouse_id) {
  1664. foreach ($stockData as $warehouse_id => $warehouse) {
  1665. if ($warehouse_id != $selected_warehouse_id) {
  1666. unset($stockData[$warehouse_id]);
  1667. }
  1668. }
  1669. }
  1670. return array('stock_warehouses'=>$stockData);
  1671. }
  1672. // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
  1673. /**
  1674. * Clean sensible object datas
  1675. *
  1676. * @param Object $object Object to clean
  1677. * @return Object Object with cleaned properties
  1678. */
  1679. protected function _cleanObjectDatas($object)
  1680. {
  1681. // phpcs:enable
  1682. $object = parent::_cleanObjectDatas($object);
  1683. unset($object->statut);
  1684. unset($object->regeximgext);
  1685. unset($object->price_by_qty);
  1686. unset($object->prices_by_qty_id);
  1687. unset($object->libelle);
  1688. unset($object->product_id_already_linked);
  1689. unset($object->reputations);
  1690. unset($object->db);
  1691. unset($object->name);
  1692. unset($object->firstname);
  1693. unset($object->lastname);
  1694. unset($object->civility_id);
  1695. unset($object->contact);
  1696. unset($object->contact_id);
  1697. unset($object->thirdparty);
  1698. unset($object->user);
  1699. unset($object->origin);
  1700. unset($object->origin_id);
  1701. unset($object->fourn_pu);
  1702. unset($object->fourn_price_base_type);
  1703. unset($object->fourn_socid);
  1704. unset($object->ref_fourn);
  1705. unset($object->ref_supplier);
  1706. unset($object->product_fourn_id);
  1707. unset($object->fk_project);
  1708. unset($object->mode_reglement_id);
  1709. unset($object->cond_reglement_id);
  1710. unset($object->demand_reason_id);
  1711. unset($object->transport_mode_id);
  1712. unset($object->cond_reglement);
  1713. unset($object->shipping_method_id);
  1714. unset($object->model_pdf);
  1715. unset($object->note);
  1716. unset($object->nbphoto);
  1717. unset($object->recuperableonly);
  1718. unset($object->multiprices_recuperableonly);
  1719. unset($object->tva_npr);
  1720. unset($object->lines);
  1721. unset($object->fk_bank);
  1722. unset($object->fk_account);
  1723. unset($object->supplierprices); // Mut use another API to get them
  1724. if (empty(DolibarrApiAccess::$user->rights->stock->lire)) {
  1725. unset($object->stock_reel);
  1726. unset($object->stock_theorique);
  1727. unset($object->stock_warehouse);
  1728. }
  1729. return $object;
  1730. }
  1731. /**
  1732. * Validate fields before create or update object
  1733. *
  1734. * @param array $data Datas to validate
  1735. * @return array
  1736. * @throws RestException
  1737. */
  1738. private function _validate($data)
  1739. {
  1740. $product = array();
  1741. foreach (Products::$FIELDS as $field) {
  1742. if (!isset($data[$field])) {
  1743. throw new RestException(400, "$field field missing");
  1744. }
  1745. $product[$field] = $data[$field];
  1746. }
  1747. return $product;
  1748. }
  1749. /**
  1750. * Get properties of 1 product object.
  1751. * Return an array with product information.
  1752. *
  1753. * @param int $id ID of product
  1754. * @param string $ref Ref of element
  1755. * @param string $ref_ext Ref ext of element
  1756. * @param string $barcode Barcode of element
  1757. * @param int $includestockdata Load also information about stock (slower)
  1758. * @param bool $includesubproducts Load information about subproducts (if product is a virtual product)
  1759. * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product)
  1760. * @param bool $includeifobjectisused Check if product object is used and set property 'is_object_used' with result.
  1761. * @param bool $includetrans Load also the translations of product label and description
  1762. * @return array|mixed Data without useless information
  1763. *
  1764. * @throws RestException 401
  1765. * @throws RestException 403
  1766. * @throws RestException 404
  1767. */
  1768. private function _fetch($id, $ref = '', $ref_ext = '', $barcode = '', $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includeifobjectisused = false, $includetrans = false)
  1769. {
  1770. if (empty($id) && empty($ref) && empty($ref_ext) && empty($barcode)) {
  1771. throw new RestException(400, 'bad value for parameter id, ref, ref_ext or barcode');
  1772. }
  1773. $id = (empty($id) ? 0 : $id);
  1774. if (!DolibarrApiAccess::$user->rights->produit->lire) {
  1775. throw new RestException(403);
  1776. }
  1777. $result = $this->product->fetch($id, $ref, $ref_ext, $barcode, 0, 0, ($includetrans ? 0 : 1));
  1778. if (!$result) {
  1779. throw new RestException(404, 'Product not found');
  1780. }
  1781. if (!DolibarrApi::_checkAccessToResource('product', $this->product->id)) {
  1782. throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
  1783. }
  1784. if (!empty($includestockdata) && DolibarrApiAccess::$user->rights->stock->lire) {
  1785. $this->product->load_stock($includestockdata);
  1786. if (is_array($this->product->stock_warehouse)) {
  1787. foreach ($this->product->stock_warehouse as $keytmp => $valtmp) {
  1788. if (isset($this->product->stock_warehouse[$keytmp]->detail_batch) && is_array($this->product->stock_warehouse[$keytmp]->detail_batch)) {
  1789. foreach ($this->product->stock_warehouse[$keytmp]->detail_batch as $keytmp2 => $valtmp2) {
  1790. unset($this->product->stock_warehouse[$keytmp]->detail_batch[$keytmp2]->db);
  1791. }
  1792. }
  1793. }
  1794. }
  1795. }
  1796. if ($includesubproducts) {
  1797. $childsArbo = $this->product->getChildsArbo($id, 1);
  1798. $keys = array('rowid', 'qty', 'fk_product_type', 'label', 'incdec', 'ref', 'fk_association', 'rang');
  1799. $childs = array();
  1800. foreach ($childsArbo as $values) {
  1801. $childs[] = array_combine($keys, $values);
  1802. }
  1803. $this->product->sousprods = $childs;
  1804. }
  1805. if ($includeparentid) {
  1806. $prodcomb = new ProductCombination($this->db);
  1807. $this->product->fk_product_parent = null;
  1808. if (($fk_product_parent = $prodcomb->fetchByFkProductChild($this->product->id)) > 0) {
  1809. $this->product->fk_product_parent = $fk_product_parent;
  1810. }
  1811. }
  1812. if ($includeifobjectisused) {
  1813. $this->product->is_object_used = ($this->product->isObjectUsed() > 0);
  1814. }
  1815. return $this->_cleanObjectDatas($this->product);
  1816. }
  1817. }