dolibarr 24.0.2
api_recruitments.class.php
1<?php
2/* Copyright (C) 2022 Thibault FOUCART <support@ptibogxiv.net>
3 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
4 * Copyright (C) 2025 Frédéric France <frederic.france@free.fr>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20use Luracast\Restler\RestException;
21
22dol_include_once('/recruitment/class/recruitmentjobposition.class.php');
23dol_include_once('/recruitment/class/recruitmentcandidature.class.php');
24
25
26
40{
44 public $jobposition;
48 public $candidature;
49
50
56 public function __construct()
57 {
58 global $db;
59 $this->db = $db;
60 $this->jobposition = new RecruitmentJobPosition($this->db);
61 $this->candidature = new RecruitmentCandidature($this->db);
62 }
63
64
78 public function getJobPosition($id)
79 {
80 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'read')) {
81 throw new RestException(403);
82 }
83
84 $result = $this->jobposition->fetch($id);
85 if (!$result) {
86 throw new RestException(404, 'JobPosition not found');
87 }
88
89 if (!DolibarrApi::_checkAccessToResource('recruitment', $this->jobposition->id, 'recruitment_recruitmentjobposition')) {
90 throw new RestException(403, 'Access to instance id='.$this->jobposition->id.' of object not allowed for login '.DolibarrApiAccess::$user->login);
91 }
92
93 return $this->_cleanObjectDatas($this->jobposition);
94 }
95
109 public function getCandidature($id)
110 {
111 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'read')) {
112 throw new RestException(403);
113 }
114
115 $result = $this->candidature->fetch($id);
116 if (!$result) {
117 throw new RestException(404, 'Candidature not found');
118 }
119
120 if (!DolibarrApi::_checkAccessToResource('recruitment', $this->candidature->id, 'recruitment_recruitmentcandidature')) {
121 throw new RestException(403, 'Access to instance id='.$this->candidature->id.' of object not allowed for login '.DolibarrApiAccess::$user->login);
122 }
123
124 return $this->_cleanObjectDatas($this->candidature);
125 }
126
147 public function indexJobPosition($sortfield = "t.rowid", $sortorder = 'ASC', $limit = 100, $page = 0, $sqlfilters = '', $properties = '', $pagination_data = false)
148 {
149 $obj_ret = array();
150 $tmpobject = new RecruitmentJobPosition($this->db);
151
152 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'read')) {
153 throw new RestException(403);
154 }
155
156 $socid = DolibarrApiAccess::$user->socid ?: 0;
157
158 $restrictonsocid = 1; // RecruitmentJobPosition::$fields has a 'fk_soc' field
159
160 // If the internal user must only see his customers, force searching by him
161 $search_sale = 0;
162 if ($restrictonsocid && !DolibarrApiAccess::$user->hasRight('societe', 'client', 'voir') && !$socid) {
163 $search_sale = DolibarrApiAccess::$user->id;
164 }
165
166 $sql = "SELECT t.rowid";
167 $sql .= " FROM ".MAIN_DB_PREFIX.$tmpobject->table_element." AS t";
168 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmpobject->table_element."_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
169 $sql .= " WHERE 1 = 1";
170 if ($tmpobject->ismultientitymanaged) {
171 $sql .= ' AND t.entity IN ('.getEntity($tmpobject->element).')';
172 }
173 if ($restrictonsocid && $socid) {
174 $sql .= " AND t.fk_soc = ".((int) $socid);
175 }
176 // Search on sale representative
177 if ($search_sale && $search_sale != '-1') {
178 if ($search_sale == -2) {
179 $sql .= " AND NOT EXISTS (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_soc = t.fk_soc)";
180 } elseif ($search_sale > 0) {
181 // fk_soc is optional on RecruitmentJobPosition (a position isn't necessarily tied to a
182 // customer's site), so a row with no linked thirdparty must still be visible to the sales
183 // rep rather than being hidden by an EXISTS check that can never match a null fk_soc.
184 $sql .= " AND (t.fk_soc IS NULL OR EXISTS (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_soc = t.fk_soc AND sc.fk_user = ".((int) $search_sale)."))";
185 }
186 }
187 if ($sqlfilters) {
188 $errormessage = '';
189 $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
190 if ($errormessage) {
191 throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage);
192 }
193 }
194
195 //this query will return total orders with the filters given
196 $sqlTotals = str_replace('SELECT t.rowid', 'SELECT count(t.rowid) as total', $sql);
197
198 $sql .= $this->db->order($sortfield, $sortorder);
199 if ($limit) {
200 if ($page < 0) {
201 $page = 0;
202 }
203 $offset = $limit * $page;
204
205 $sql .= $this->db->plimit($limit + 1, $offset);
206 }
207
208 $result = $this->db->query($sql);
209 $i = 0;
210 if ($result) {
211 $num = $this->db->num_rows($result);
212 $min = min($num, ($limit <= 0 ? $num : $limit));
213 while ($i < $min) {
214 $obj = $this->db->fetch_object($result);
215 $tmp_object = new RecruitmentJobPosition($this->db);
216 if ($tmp_object->fetch($obj->rowid)) {
217 $obj_ret[] = $this->_filterObjectProperties($this->_cleanObjectDatas($tmp_object), $properties);
218 }
219 $i++;
220 }
221 } else {
222 throw new RestException(503, 'Error when retrieving jobposition list: '.$this->db->lasterror());
223 }
224
225 //if $pagination_data is true the response will contain element data with all values and element pagination with pagination data(total,page,limit)
226 if ($pagination_data) {
227 $totalsResult = $this->db->query($sqlTotals);
228 $total = $this->db->fetch_object($totalsResult)->total;
229
230 $tmp = $obj_ret;
231 $obj_ret = [];
232
233 $obj_ret['data'] = $tmp;
234 $obj_ret['pagination'] = [
235 'total' => (int) $total,
236 'page' => $page, //count starts from 0
237 'page_count' => ceil((int) $total / $limit),
238 'limit' => $limit
239 ];
240 }
241
242 return $obj_ret;
243 }
244
265 public function indexCandidature($sortfield = "t.rowid", $sortorder = 'ASC', $limit = 100, $page = 0, $sqlfilters = '', $properties = '', $pagination_data = false)
266 {
267 global $db, $conf;
268
269 $obj_ret = array();
270 $tmpobject = new RecruitmentCandidature($this->db);
271
272 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'read')) {
273 throw new RestException(403);
274 }
275
276 $socid = DolibarrApiAccess::$user->socid ?: 0;
277
278 $restrictonsocid = 0; // Set to 1 if there is a field socid in table of object
279
280 // If the internal user must only see his customers, force searching by him
281 $search_sale = 0;
282 if ($restrictonsocid && !DolibarrApiAccess::$user->hasRight('societe', 'client', 'voir') && !$socid) {
283 $search_sale = DolibarrApiAccess::$user->id;
284 }
285
286 $sql = "SELECT t.rowid";
287 $sql .= " FROM ".MAIN_DB_PREFIX.$tmpobject->table_element." AS t";
288 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmpobject->table_element."_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
289 $sql .= " WHERE 1 = 1";
290 if ($tmpobject->ismultientitymanaged) {
291 $sql .= ' AND t.entity IN ('.getEntity($tmpobject->element).')';
292 }
293 if ($restrictonsocid && $socid) {
294 $sql .= " AND t.fk_soc = ".((int) $socid);
295 }
296 // Search on sale representative
297 if ($search_sale && $search_sale != '-1') {
298 if ($search_sale == -2) {
299 $sql .= " AND NOT EXISTS (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_soc = t.fk_soc)";
300 } elseif ($search_sale > 0) {
301 $sql .= " AND EXISTS (SELECT sc.fk_soc FROM ".MAIN_DB_PREFIX."societe_commerciaux as sc WHERE sc.fk_soc = t.fk_soc AND sc.fk_user = ".((int) $search_sale).")";
302 }
303 }
304 if ($sqlfilters) {
305 $errormessage = '';
306 $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
307 if ($errormessage) {
308 throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage);
309 }
310 }
311
312 //this query will return total orders with the filters given
313 $sqlTotals = str_replace('SELECT t.rowid', 'SELECT count(t.rowid) as total', $sql);
314
315 $sql .= $this->db->order($sortfield, $sortorder);
316 if ($limit) {
317 if ($page < 0) {
318 $page = 0;
319 }
320 $offset = $limit * $page;
321
322 $sql .= $this->db->plimit($limit + 1, $offset);
323 }
324
325 $result = $this->db->query($sql);
326 $i = 0;
327 if ($result) {
328 $num = $this->db->num_rows($result);
329 $min = min($num, ($limit <= 0 ? $num : $limit));
330 while ($i < $min) {
331 $obj = $this->db->fetch_object($result);
332 $tmp_object = new RecruitmentCandidature($this->db);
333 if ($tmp_object->fetch($obj->rowid)) {
334 $obj_ret[] = $this->_filterObjectProperties($this->_cleanObjectDatas($tmp_object), $properties);
335 }
336 $i++;
337 }
338 } else {
339 throw new RestException(503, 'Error when retrieving candidature list: '.$this->db->lasterror());
340 }
341
342 //if $pagination_data is true the response will contain element data with all values and element pagination with pagination data(total,page,limit)
343 if ($pagination_data) {
344 $totalsResult = $this->db->query($sqlTotals);
345 $total = $this->db->fetch_object($totalsResult)->total;
346
347 $tmp = $obj_ret;
348 $obj_ret = [];
349
350 $obj_ret['data'] = $tmp;
351 $obj_ret['pagination'] = [
352 'total' => (int) $total,
353 'page' => $page, //count starts from 0
354 'page_count' => ceil((int) $total / $limit),
355 'limit' => $limit
356 ];
357 }
358
359 return $obj_ret;
360 }
361
374 public function postJobPosition($request_data = null)
375 {
376 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'write')) {
377 throw new RestException(403);
378 }
379
380 // Check mandatory fields
381 $result = $this->_validate($request_data, $this->jobposition);
382
383 foreach ($request_data as $field => $value) {
384 if ($field === 'caller') {
385 // 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 with the caller
386 $this->jobposition->context['caller'] = sanitizeVal($request_data['caller'], 'aZ09');
387 continue;
388 }
389
390 $this->jobposition->$field = $this->_checkValForAPI($field, $value, $this->jobposition);
391 }
392
393 // Clean data
394 // $this->jobposition->abc = sanitizeVal($this->jobposition->abc, 'alphanohtml');
395
396 if ($this->jobposition->create(DolibarrApiAccess::$user) < 0) {
397 throw new RestException(500, "Error creating jobposition", array_merge(array($this->jobposition->error), $this->jobposition->errors));
398 }
399 return $this->jobposition->id;
400 }
401
414 public function postCandidature($request_data = null)
415 {
416 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'write')) {
417 throw new RestException(403);
418 }
419
420 // Check mandatory fields. Validate against the candidature model so the API
421 // rejects with the actual missing candidature fields (e.g. email) instead of
422 // the unrelated job position fields (see issue #38429).
423 $result = $this->_validate($request_data, $this->candidature);
424
425 foreach ($request_data as $field => $value) {
426 if ($field === 'caller') {
427 // 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 with the caller
428 $this->candidature->context['caller'] = sanitizeVal($request_data['caller'], 'aZ09');
429 continue;
430 }
431
432 $this->candidature->$field = $this->_checkValForAPI($field, $value, $this->candidature);
433 }
434
435 // Clean data
436 // $this->jobposition->abc = sanitizeVal($this->jobposition->abc, 'alphanohtml');
437
438 if ($this->candidature->create(DolibarrApiAccess::$user) < 0) {
439 throw new RestException(500, "Error creating candidature", array_merge(array($this->candidature->error), $this->candidature->errors));
440 }
441 return $this->candidature->id;
442 }
443
457 public function putJobPosition($id, $request_data = null)
458 {
459 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'write')) {
460 throw new RestException(403);
461 }
462
463 $result = $this->jobposition->fetch($id);
464 if (!$result) {
465 throw new RestException(404, 'jobposition not found');
466 }
467
468 if (!DolibarrApi::_checkAccessToResource('recruitment', $this->jobposition->id, 'recruitment_recruitmentjobposition')) {
469 throw new RestException(403, 'Access to instance id='.$this->jobposition->id.' of object not allowed for login '.DolibarrApiAccess::$user->login);
470 }
471
472 foreach ($request_data as $field => $value) {
473 if ($field == 'id') {
474 continue;
475 }
476 if ($field === 'caller') {
477 // 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 with the caller
478 $this->jobposition->context['caller'] = sanitizeVal($request_data['caller'], 'aZ09');
479 continue;
480 }
481
482 $this->jobposition->$field = $this->_checkValForAPI($field, $value, $this->jobposition);
483 }
484
485 // Clean data
486 // $this->jobposition->abc = sanitizeVal($this->jobposition->abc, 'alphanohtml');
487
488 if ($this->jobposition->update(DolibarrApiAccess::$user, 0) > 0) {
489 return $this->getJobPosition($id);
490 } else {
491 throw new RestException(500, $this->jobposition->error);
492 }
493 }
494
508 public function putCandidature($id, $request_data = null)
509 {
510 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'write')) {
511 throw new RestException(403);
512 }
513
514 $result = $this->candidature->fetch($id);
515 if (!$result) {
516 throw new RestException(404, 'candidature not found');
517 }
518
519 if (!DolibarrApi::_checkAccessToResource('recruitment', $this->candidature->id, 'recruitment_recruitmentcandidature')) {
520 throw new RestException(403, 'Access to instance id='.$this->candidature->id.' of object not allowed for login '.DolibarrApiAccess::$user->login);
521 }
522
523 foreach ($request_data as $field => $value) {
524 if ($field == 'id') {
525 continue;
526 }
527 if ($field === 'caller') {
528 // 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 with the caller
529 $this->candidature->context['caller'] = sanitizeVal($request_data['caller'], 'aZ09');
530 continue;
531 }
532
533 $this->candidature->$field = $this->_checkValForAPI($field, $value, $this->candidature);
534 }
535
536 // Clean data
537 // $this->jobposition->abc = sanitizeVal($this->jobposition->abc, 'alphanohtml');
538
539 if ($this->candidature->update(DolibarrApiAccess::$user, 0) > 0) {
540 return $this->getCandidature($id);
541 } else {
542 throw new RestException(500, $this->candidature->error);
543 }
544 }
545
546
559 public function deleteJobPosition($id)
560 {
561 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'delete')) {
562 throw new RestException(403);
563 }
564 $result = $this->jobposition->fetch($id);
565 if (!$result) {
566 throw new RestException(404, 'jobposition not found');
567 }
568
569 if (!DolibarrApi::_checkAccessToResource('recruitment', $this->jobposition->id, 'recruitment_recruitmentjobposition')) {
570 throw new RestException(403, 'Access to instance id='.$this->jobposition->id.' of object not allowed for login '.DolibarrApiAccess::$user->login);
571 }
572
573 if (!$this->jobposition->delete(DolibarrApiAccess::$user)) {
574 throw new RestException(500, 'Error when deleting jobposition : '.$this->jobposition->error);
575 }
576
577 return array(
578 'success' => array(
579 'code' => 200,
580 'message' => 'jobposition deleted'
581 )
582 );
583 }
584
597 public function deleteCandidature($id)
598 {
599 if (!DolibarrApiAccess::$user->hasRight('recruitment', 'recruitmentjobposition', 'delete')) {
600 throw new RestException(403);
601 }
602 $result = $this->candidature->fetch($id);
603 if (!$result) {
604 throw new RestException(404, 'candidature not found');
605 }
606
607 if (!DolibarrApi::_checkAccessToResource('recruitment', $this->candidature->id, 'recruitment_recruitmentcandidature')) {
608 throw new RestException(403, 'Access to instance id='.$this->candidature->id.' of object not allowed for login '.DolibarrApiAccess::$user->login);
609 }
610
611 if (!$this->candidature->delete(DolibarrApiAccess::$user)) {
612 throw new RestException(500, 'Error when deleting candidature : '.$this->candidature->error);
613 }
614
615 return array(
616 'success' => array(
617 'code' => 200,
618 'message' => 'candidature deleted'
619 )
620 );
621 }
622
623
624 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
634 protected function _cleanObjectDatas($object)
635 {
636 // phpcs:enable
637 $object = parent::_cleanObjectDatas($object);
638
639 unset($object->rowid);
640 unset($object->canvas);
641
642 /*unset($object->name);
643 unset($object->lastname);
644 unset($object->firstname);
645 unset($object->civility_id);
646 unset($object->statut);
647 unset($object->state);
648 unset($object->state_id);
649 unset($object->state_code);
650 unset($object->region);
651 unset($object->region_code);
652 unset($object->country);
653 unset($object->country_id);
654 unset($object->country_code);
655 unset($object->barcode_type);
656 unset($object->barcode_type_code);
657 unset($object->barcode_type_label);
658 unset($object->barcode_type_coder);
659 unset($object->total_ht);
660 unset($object->total_tva);
661 unset($object->total_localtax1);
662 unset($object->total_localtax2);
663 unset($object->total_ttc);
664 unset($object->fk_account);
665 unset($object->comments);
666 unset($object->note);
667 unset($object->mode_reglement_id);
668 unset($object->cond_reglement_id);
669 unset($object->cond_reglement);
670 unset($object->shipping_method_id);
671 unset($object->fk_incoterms);
672 unset($object->label_incoterms);
673 unset($object->location_incoterms);
674 */
675
676 // If object has lines, remove $db property
677 if (isset($object->lines) && is_array($object->lines) && count($object->lines) > 0) {
678 $nboflines = count($object->lines);
679 for ($i = 0; $i < $nboflines; $i++) {
680 $this->_cleanObjectDatas($object->lines[$i]);
681
682 unset($object->lines[$i]->lines);
683 unset($object->lines[$i]->note);
684 }
685 }
686
687 return $object;
688 }
689
699 private function _validate($data, $object)
700 {
701 if ($data === null) {
702 $data = array();
703 }
704 $result = array();
705 foreach ($object->fields as $field => $propfield) {
706 if (in_array($field, array('rowid', 'entity', 'date_creation', 'tms', 'fk_user_creat', 'ref')) || empty($propfield['notnull']) || $propfield['notnull'] != 1 || !empty($propfield['noteditable']) || isset($propfield['default'])) {
707 continue; // Not a mandatory field or auto-generated field or has default value
708 }
709 if (!isset($data[$field])) {
710 throw new RestException(400, "$field field missing");
711 }
712 $result[$field] = $data[$field];
713 }
714 return $result;
715 }
716}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
Class for API REST v1.
Definition api.class.php:35
_filterObjectProperties($object, $properties)
Filter properties that will be returned on object.
_checkValForAPI($field, $value, $object)
Check and convert a string depending on its type/name.
static _checkAccessToResource($resource, $resource_id=0, $dbtablename='', $feature2='', $dbt_keyfield='fk_soc', $dbt_select='rowid', $parenttableforentity='')
Check access by user to a given resource.
Class for RecruitmentCandidature.
Class for RecruitmentJobPosition.
getJobPosition($id)
Get properties of a jobposition object.
indexJobPosition($sortfield="t.rowid", $sortorder='ASC', $limit=100, $page=0, $sqlfilters='', $properties='', $pagination_data=false)
List jobpositions.
deleteJobPosition($id)
Delete jobposition.
__construct()
Constructor.
putCandidature($id, $request_data=null)
Update candidature.
deleteCandidature($id)
Delete candidature.
postJobPosition($request_data=null)
Create jobposition object.
_cleanObjectDatas($object)
Clean sensible object datas @phpstan-template T.
putJobPosition($id, $request_data=null)
Update jobposition.
_validate($data, $object)
Validate fields before create or update object.
getCandidature($id)
Get properties of a candidature object.
indexCandidature($sortfield="t.rowid", $sortorder='ASC', $limit=100, $page=0, $sqlfilters='', $properties='', $pagination_data=false)
List candatures.
postCandidature($request_data=null)
Create candidature object.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
if(!function_exists( 'dol_getprefix')) dol_include_once($relpath, $classname='')
Make an include_once using default root and alternate root if it fails.
forgeSQLFromUniversalSearchCriteria($filter, &$errorstr='', $noand=0, $nopar=0, $noerror=0, $forbiddenfields=array())
forgeSQLFromUniversalSearchCriteria
sanitizeVal($out='', $check='alphanohtml', $filter=null, $options=null)
Return a sanitized or empty value after checking value against a rule.