dolibarr 22.0.5
inventory.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2019 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2025 MDW <mdeweerd@users.noreply.github.com>
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
26// Load Dolibarr environment
27require '../../main.inc.php';
28include_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php';
29include_once DOL_DOCUMENT_ROOT.'/product/class/html.formproduct.class.php';
30include_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
31include_once DOL_DOCUMENT_ROOT.'/product/inventory/class/inventory.class.php';
32include_once DOL_DOCUMENT_ROOT.'/product/inventory/lib/inventory.lib.php';
33include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
34include_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
35
44// Load translation files required by the page
45$langs->loadLangs(array("stocks", "other", "productbatch"));
46
47// Get parameters
48$id = GETPOSTINT('id');
49$ref = GETPOST('ref', 'alpha');
50$action = GETPOST('action', 'aZ09');
51$confirm = GETPOST('confirm', 'alpha');
52$cancel = GETPOST('cancel');
53$contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'inventorycard'; // To manage different context of search
54$backtopage = GETPOST('backtopage', 'alpha');
55$listoffset = GETPOST('listoffset', 'alpha');
56$sortfield = GETPOST('sortfield', 'aZ09comma');
57$sortorder = GETPOST('sortorder', 'aZ09comma');
58$limit = GETPOSTINT('limit') > 0 ? GETPOSTINT('limit') : $conf->liste_limit;
59$page = GETPOSTISSET('pageplusone') ? (GETPOSTINT('pageplusone') - 1) : GETPOSTINT("page");
60if (empty($page) || $page == -1) {
61 $page = 0;
62}
63$offset = $limit * $page;
64$pageprev = $page - 1;
65$pagenext = $page + 1;
66
67$fk_warehouse = GETPOSTINT('fk_warehouse');
68$fk_product = GETPOSTINT('fk_product');
69$lineid = GETPOSTINT('lineid');
70$batch = GETPOST('batch', 'alphanohtml');
71$totalExpectedValuation = 0;
72$totalRealValuation = 0;
73$hookmanager->initHooks(array('inventorycard')); // Note that conf->hooks_modules contains array
74if (!getDolGlobalString('MAIN_USE_ADVANCED_PERMS')) {
75 $result = restrictedArea($user, 'stock', $id, 'inventory&stock');
76} else {
77 $result = restrictedArea($user, 'stock', $id, 'inventory&stock', 'inventory_advance');
78}
79
80// Initialize a technical objects
81$object = new Inventory($db);
82$extrafields = new ExtraFields($db);
83$diroutputmassaction = $conf->stock->dir_output.'/temp/massgeneration/'.$user->id;
84
85// Default sort order (if not yet defined by previous GETPOST)
86if (!$sortfield) {
87 $sortfield = "e.ref";
88}
89if (!$sortorder) {
90 $sortorder = "ASC";
91}
92
93// Fetch optionals attributes and labels
94$extrafields->fetch_name_optionals_label($object->table_element);
95
96$search_array_options = $extrafields->getOptionalsFromPost($object->table_element, '', 'search_');
97
98// Initialize array of search criteria
99$search_all = GETPOST("search_all", 'alpha');
100$search = array();
101foreach ($object->fields as $key => $val) {
102 if (GETPOST('search_'.$key, 'alpha')) {
103 $search[$key] = GETPOST('search_'.$key, 'alpha');
104 }
105}
106
107if (empty($action) && empty($id) && empty($ref)) {
108 $action = 'view';
109}
110
111// Load object
112include DOL_DOCUMENT_ROOT.'/core/actions_fetchobject.inc.php'; // Must be 'include', not 'include_once'.
113
114// Security check - Protection if external user
115//if ($user->socid > 0) accessforbidden();
116//if ($user->socid > 0) $socid = $user->socid;
117//$result = restrictedArea($user, 'mymodule', $id);
118
119//Parameters Page
120$paramwithsearch = '&sortfield=' . urlencode($sortfield);
121$paramwithsearch .= '&sortorder=' . urlencode($sortorder);
122if ($limit > 0 && $limit != $conf->liste_limit) {
123 $paramwithsearch .= '&limit='.((int) $limit);
124}
125
126// Sort by warehouse/product or product/warehouse
127$sortfield .= ',' . ($sortfield == 'e.ref' ? 'p.ref' : 'e.ref') . ',id.batch,id.rowid';
128$sortorder .= ',' . $sortorder.",ASC,ASC";
129
130if (!getDolGlobalString('MAIN_USE_ADVANCED_PERMS')) {
131 $permissiontoadd = $user->hasRight('stock', 'creer');
132 $permissiontodelete = $user->hasRight('stock', 'supprimer');
133 $permissiontoupdatestock = $user->hasRight('stock', 'mouvement', 'creer');
134} else {
135 $permissiontoadd = $user->hasRight('stock', 'inventory_advance', 'write');
136 $permissiontodelete = $user->hasRight('stock', 'inventory_advance', 'write');
137 $permissiontoupdatestock = $user->hasRight('stock', 'inventory_advance', 'write');
138}
139
140$now = dol_now();
141
142
143
144/*
145 * Actions
146 */
147
148if ($cancel) {
149 $action = '';
150}
151
152
153$parameters = array();
154$reshook = $hookmanager->executeHooks('doActions', $parameters, $object, $action); // Note that $action and $object may have been modified by some hooks
155if ($reshook < 0) {
156 setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
157}
158
159if (empty($reshook)) {
160 $error = 0;
161
162 if ($action == 'cancel_record' && $permissiontoupdatestock) {
163 $object->setCanceled($user);
164 }
165
166 // Close inventory by recording the stock movements
167 if ($action == 'update' && $permissiontoupdatestock && $object->status == $object::STATUS_VALIDATED) {
168 $stockmovment = new MouvementStock($db);
169 $stockmovment->setOrigin($object->element, $object->id);
170
171 $cacheOfProducts = array();
172
173 $db->begin();
174
175 $sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
176 $sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated, id.pmp_real';
177 $sql .= ' FROM '.MAIN_DB_PREFIX.'inventorydet as id';
178 $sql .= ' WHERE id.fk_inventory = '.((int) $object->id);
179 $sql .= ' ORDER BY id.rowid';
180
181 $resql = $db->query($sql);
182 if ($resql) {
183 $num = $db->num_rows($resql);
184 $i = 0;
185 $totalarray = array();
186 $option = '';
187
188 while ($i < $num) {
189 $line = $db->fetch_object($resql);
190
191 $qty_stock = $line->qty_stock;
192 $qty_view = $line->qty_view; // The quantity viewed by inventorier, the qty we target
193
194
195 // Load real stock we have now.
196 if (isset($cacheOfProducts[$line->fk_product])) {
197 $product_static = $cacheOfProducts[$line->fk_product];
198 } else {
199 $product_static = new Product($db);
200 $result = $product_static->fetch($line->fk_product, '', '', '', 1, 1, 1);
201
202 //$option = 'nobatch';
203 $option .= ',novirtual';
204 $product_static->load_stock($option); // Load stock_reel + stock_warehouse.
205
206 $cacheOfProducts[$product_static->id] = $product_static;
207 }
208
209 // Get the real quantity in stock now, but before the stock move for inventory.
210 $realqtynow = $product_static->stock_warehouse[$line->fk_warehouse]->real;
211 if (isModEnabled('productbatch') && $product_static->hasbatch()) {
212 $realqtynow = $product_static->stock_warehouse[$line->fk_warehouse]->detail_batch[$line->batch]->qty;
213 }
214
215 if (!is_null($qty_view)) {
216 $stock_movement_qty = price2num($qty_view - $realqtynow, 'MS');
217 //print "Process inventory line ".$line->rowid." product=".$product_static->id." realqty=".$realqtynow." qty_stock=".$qty_stock." qty_view=".$qty_view." warehouse=".$line->fk_warehouse." qty to move=".$stock_movement_qty."<br>\n";
218
219 if ($stock_movement_qty != 0) {
220 if ($stock_movement_qty < 0) {
221 $movement_type = 1;
222 } else {
223 $movement_type = 0;
224 }
225
226 $datemovement = '';
227 //$inventorycode = 'INV'.$object->id;
228 $inventorycode = 'INV-'.$object->ref;
229 $price = 0;
230 if (!empty($line->pmp_real) && getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
231 $price = $line->pmp_real;
232 }
233
234 $idstockmove = $stockmovment->_create($user, $line->fk_product, $line->fk_warehouse, (float) $stock_movement_qty, $movement_type, $price, $langs->trans('LabelOfInventoryMovemement', $object->ref), $inventorycode, $datemovement, '', '', $line->batch);
235 if ($idstockmove < 0) {
236 $error++;
237 setEventMessages($stockmovment->error, $stockmovment->errors, 'errors');
238 break;
239 }
240
241 // Update line with id of stock movement (and the start quantity if it has changed this last recording)
242 $sqlupdate = "UPDATE ".MAIN_DB_PREFIX."inventorydet";
243 $sqlupdate .= " SET fk_movement = ".((int) $idstockmove);
244 if ($qty_stock != $realqtynow) {
245 $sqlupdate .= ", qty_stock = ".((float) $realqtynow);
246 }
247 $sqlupdate .= " WHERE rowid = ".((int) $line->rowid);
248 $resqlupdate = $db->query($sqlupdate);
249 if (! $resqlupdate) {
250 $error++;
251 setEventMessages($db->lasterror(), null, 'errors');
252 break;
253 }
254 }
255
256 if (!empty($line->pmp_real) && getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
257 $sqlpmp = 'UPDATE '.MAIN_DB_PREFIX.'product SET pmp = '.((float) $line->pmp_real).' WHERE rowid = '.((int) $line->fk_product);
258 $resqlpmp = $db->query($sqlpmp);
259 if (! $resqlpmp) {
260 $error++;
261 setEventMessages($db->lasterror(), null, 'errors');
262 break;
263 }
264 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
265 $sqlpmp = 'UPDATE '.MAIN_DB_PREFIX.'product_perentity SET pmp = '.((float) $line->pmp_real).' WHERE fk_product = '.((int) $line->fk_product).' AND entity='.$conf->entity;
266 $resqlpmp = $db->query($sqlpmp);
267 if (! $resqlpmp) {
268 $error++;
269 setEventMessages($db->lasterror(), null, 'errors');
270 break;
271 }
272 }
273 }
274 }
275 $i++;
276 }
277
278 if (!$error) {
279 $object->setRecorded($user);
280 }
281 } else {
282 setEventMessages($db->lasterror, null, 'errors');
283 $error++;
284 }
285
286 if (! $error) {
287 $db->commit();
288 } else {
289 $db->rollback();
290 }
291 $action = '';
292 }
293
294 // Save quantity found during inventory (when we click on Save button on inventory page)
295 if ($action == 'updateinventorylines' && $permissiontoupdatestock) {
296 $sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
297 $sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated';
298 $sql .= ' FROM '.MAIN_DB_PREFIX.'inventorydet as id';
299 $sql .= ' LEFT JOIN ' . $db->prefix() . 'product as p ON id.fk_product = p.rowid';
300 $sql .= ' LEFT JOIN ' . $db->prefix() . 'entrepot as e ON id.fk_warehouse = e.rowid';
301 $sql .= ' WHERE id.fk_inventory = '.((int) $object->id);
302 $sql .= $db->order($sortfield, $sortorder);
303 $sql .= $db->plimit($limit, $offset);
304
305 $db->begin();
306
307 $resql = $db->query($sql);
308 if ($resql) {
309 $num = $db->num_rows($resql);
310 $i = 0;
311 $totalarray = array();
312 $inventoryline = new InventoryLine($db);
313
314 while ($i < $num) {
315 $line = $db->fetch_object($resql);
316 $lineid = $line->rowid;
317
318 $result = 0;
319 $resultupdate = 0;
320
321 if (GETPOST("id_".$lineid, 'alpha') != '') { // If a value was set ('0' or something else)
322 $qtytoupdate = (float) price2num(GETPOST("id_".$lineid, 'alpha'), 'MS');
323 $result = $inventoryline->fetch($lineid);
324 if ($qtytoupdate < 0) {
325 $result = -1;
326 setEventMessages($langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv("RealQty")), null, 'errors');
327 }
328 if ($result > 0) {
329 $inventoryline->qty_stock = (float) price2num(GETPOST('stock_qty_'.$lineid, 'alpha'), 'MS'); // The new value that was set in as hidden field
330 $inventoryline->qty_view = $qtytoupdate; // The new value we want
331 $inventoryline->pmp_real = price2num(GETPOST('realpmp_'.$lineid, 'alpha'), 'MS');
332 $inventoryline->pmp_expected = price2num(GETPOST('expectedpmp_'.$lineid, 'alpha'), 'MS');
333 $resultupdate = $inventoryline->update($user);
334 }
335 } elseif (GETPOSTISSET('id_' . $lineid)) {
336 // Delete record
337 $result = $inventoryline->fetch($lineid);
338 if ($result > 0) {
339 $inventoryline->qty_view = null; // The new value we want
340 $inventoryline->pmp_real = price2num(GETPOST('realpmp_'.$lineid, 'alpha'), 'MS');
341 $inventoryline->pmp_expected = price2num(GETPOST('expectedpmp_'.$lineid, 'alpha'), 'MS');
342 $resultupdate = $inventoryline->update($user);
343 }
344 }
345
346 if ($result < 0 || $resultupdate < 0) {
347 $error++;
348 }
349
350 $i++;
351 }
352 }
353
354 // Update line with id of stock movement (and the start quantity if it has changed this last recording)
355 if (! $error) {
356 $sqlupdate = "UPDATE ".MAIN_DB_PREFIX."inventory";
357 $sqlupdate .= " SET fk_user_modif = ".((int) $user->id);
358 $sqlupdate .= " WHERE rowid = ".((int) $object->id);
359 $resqlupdate = $db->query($sqlupdate);
360 if (! $resqlupdate) {
361 $error++;
362 setEventMessages($db->lasterror(), null, 'errors');
363 }
364 }
365
366 if (!$error) {
367 $db->commit();
368 } else {
369 $db->rollback();
370 }
371 }
372
373 $backurlforlist = DOL_URL_ROOT.'/product/inventory/list.php';
374 $backtopage = DOL_URL_ROOT.'/product/inventory/inventory.php?id='.$object->id.'&page='.$page.$paramwithsearch;
375
376 // Actions cancel, add, update, delete or clone
377 include DOL_DOCUMENT_ROOT.'/core/actions_addupdatedelete.inc.php';
378
379 // Actions when linking object each other
380 include DOL_DOCUMENT_ROOT.'/core/actions_dellink.inc.php';
381
382 // Actions when printing a doc from card
383 include DOL_DOCUMENT_ROOT.'/core/actions_printing.inc.php';
384
385 // Actions to send emails
386 /*$triggersendname = 'MYOBJECT_SENTBYMAIL';
387 $autocopy='MAIN_MAIL_AUTOCOPY_MYOBJECT_TO';
388 $trackid='stockinv'.$object->id;
389 include DOL_DOCUMENT_ROOT.'/core/actions_sendmails.inc.php';*/
390
391 if (GETPOST('addline', 'alpha')) {
392 $qty = (GETPOST('qtytoadd') != '' ? ((float) price2num(GETPOST('qtytoadd'), 'MS')) : null);
393 if ($fk_warehouse <= 0) {
394 $error++;
395 setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Warehouse")), null, 'errors');
396 }
397 if ($fk_product <= 0) {
398 $error++;
399 setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Product")), null, 'errors');
400 }
401 if (price2num(GETPOST('qtytoadd'), 'MS') < 0) {
402 $error++;
403 setEventMessages($langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv("RealQty")), null, 'errors');
404 }
405 if (!$error && isModEnabled('productbatch')) {
406 $tmpproduct = new Product($db);
407 $result = $tmpproduct->fetch($fk_product);
408
409 if (empty($error) && $tmpproduct->status_batch > 0 && empty($batch)) {
410 $error++;
411 $langs->load("errors");
412 setEventMessages($langs->trans("ErrorProductNeedBatchNumber", $tmpproduct->ref), null, 'errors');
413 }
414 if (empty($error) && $tmpproduct->status_batch == 2 && !empty($batch) && $qty > 1) {
415 $error++;
416 $langs->load("errors");
417 setEventMessages($langs->trans("TooManyQtyForSerialNumber", $tmpproduct->ref, $batch), null, 'errors');
418 }
419 if (empty($error) && empty($tmpproduct->status_batch) && !empty($batch)) {
420 $error++;
421 $langs->load("errors");
422 setEventMessages($langs->trans("ErrorProductDoesNotNeedBatchNumber", $tmpproduct->ref), null, 'errors');
423 }
424 }
425 if (!$error) {
426 $tmp = new InventoryLine($db);
427 $tmp->fk_inventory = $object->id;
428 $tmp->fk_warehouse = $fk_warehouse;
429 $tmp->fk_product = $fk_product;
430 $tmp->batch = $batch;
431 $tmp->datec = $now;
432 $tmp->qty_view = $qty;
433
434 $result = $tmp->create($user);
435 if ($result < 0) {
436 if ($db->lasterrno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
437 $langs->load("errors");
438 setEventMessages($langs->trans("ErrorRecordAlreadyExists"), null, 'errors');
439 } else {
440 dol_print_error($db, $tmp->error, $tmp->errors);
441 }
442 } else {
443 // Clear var
444 $_POST['batch'] = ''; // TODO Replace this with a var
445 $_POST['qtytoadd'] = '';
446 }
447 }
448 }
449}
450
451
452
453/*
454 * View
455 */
456
457$form = new Form($db);
458$formproduct = new FormProduct($db);
459
460$help_url = '';
461
462llxHeader('', $langs->trans('Inventory'), $help_url, '', 0, 0, '', '', '', 'mod-product page-inventory_inventory');
463
464// Part to show record
465if ($object->id <= 0) {
466 dol_print_error(null, 'Bad value for object id');
467 exit;
468}
469
470$param = '';
471if ($limit > 0 && $limit != $conf->liste_limit) {
472 $param .= '&limit=' . ((int) $limit);
473}
474
475
476$res = $object->fetch_optionals();
477
478$head = inventoryPrepareHead($object);
479print dol_get_fiche_head($head, 'inventory', $langs->trans("Inventory"), -1, 'stock');
480
481$formconfirm = '';
482
483// Confirmation to delete
484if ($action == 'delete') {
485 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('DeleteInventory'), $langs->trans('ConfirmDeleteOrder'), 'confirm_delete', '', 0, 1);
486}
487// Confirmation to delete line
488if ($action == 'deleteline') {
489 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id.'&lineid='.$lineid.'&page='.$page.$paramwithsearch, $langs->trans('DeleteLine'), $langs->trans('ConfirmDeleteLine'), 'confirm_deleteline', '', 0, 1);
490}
491
492// Clone confirmation
493if ($action == 'clone') {
494 // Create an array for form
495 $formquestion = array();
496 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('ToClone'), $langs->trans('ConfirmCloneMyObject', $object->ref), 'confirm_clone', $formquestion, 'yes', 1);
497}
498
499// Confirmation to close
500if ($action == 'record') {
501 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id.'&page='.$page.$paramwithsearch, $langs->trans('Close'), $langs->trans('ConfirmFinish'), 'update', '', 0, 1);
502 $action = 'view';
503}
504
505// Confirmation to close
506if ($action == 'confirm_cancel') {
507 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('Cancel'), $langs->trans('ConfirmCancel'), 'cancel_record', '', 0, 1);
508 $action = 'view';
509}
510
511if ($action == 'validate') {
512 $form = new Form($db);
513 $formquestion = '';
514 if (getDolGlobalInt('INVENTORY_INCLUDE_SUB_WAREHOUSE') && !empty($object->fk_warehouse)) {
515 $formquestion = array(
516 array('type' => 'checkbox', 'name' => 'include_sub_warehouse', 'label' => $langs->trans("IncludeSubWarehouse"), 'value' => 1, 'size' => '10'),
517 );
518 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('ValidateInventory'), $langs->trans('IncludeSubWarehouseExplanation'), 'confirm_validate', $formquestion, '', 1);
519 }
520}
521
522// Call Hook formConfirm
523$parameters = array('formConfirm' => $formconfirm, 'lineid' => $lineid);
524$reshook = $hookmanager->executeHooks('formConfirm', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
525if (empty($reshook)) {
526 $formconfirm .= $hookmanager->resPrint;
527} elseif ($reshook > 0) {
528 $formconfirm = $hookmanager->resPrint;
529}
530
531// Print form confirm
532print $formconfirm;
533
534
535// Object card
536// ------------------------------------------------------------
537$linkback = '<a href="'.DOL_URL_ROOT.'/product/inventory/list.php">'.$langs->trans("BackToList").'</a>';
538
539$morehtmlref = '<div class="refidno">';
540/*
541// Ref bis
542$morehtmlref.=$form->editfieldkey("RefBis", 'ref_client', $object->ref_client, $object, $user->rights->inventory->creer, 'string', '', 0, 1);
543$morehtmlref.=$form->editfieldval("RefBis", 'ref_client', $object->ref_client, $object, $user->rights->inventory->creer, 'string', '', null, null, '', 1);
544// Thirdparty
545$morehtmlref.='<br>'.$langs->trans('ThirdParty') . ' : ' . $soc->getNomUrl(1);
546// Project
547if (isModEnabled('project'))
548{
549 $langs->load("projects");
550 $morehtmlref.='<br>'.$langs->trans('Project') . ' ';
551 if ($user->rights->inventory->creer)
552 {
553 if ($action != 'classify')
554 {
555 $morehtmlref.='<a class="editfielda" href="' . $_SERVER['PHP_SELF'] . '?action=classify&token='.newToken().'&id=' . $object->id . '">' . img_edit($langs->transnoentitiesnoconv('SetProject')) . '</a> : ';
556 if ($action == 'classify') {
557 //$morehtmlref.=$form->form_project($_SERVER['PHP_SELF'] . '?id=' . $object->id, $object->socid, $object->fk_project, 'projectid', 0, 0, 1, 1);
558 $morehtmlref.='<form method="post" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'">';
559 $morehtmlref.='<input type="hidden" name="action" value="classin">';
560 $morehtmlref.='<input type="hidden" name="token" value="'.newToken().'">';
561 $morehtmlref.=$formproject->select_projects($object->socid, $object->fk_project, 'projectid', $maxlength, 0, 1, 0, 1, 0, 0, '', 1);
562 $morehtmlref.='<input type="submit" class="button valignmiddle" value="'.$langs->trans("Modify").'">';
563 $morehtmlref.='</form>';
564 } else {
565 $morehtmlref.=$form->form_project($_SERVER['PHP_SELF'] . '?id=' . $object->id, $object->socid, $object->fk_project, 'none', 0, 0, 0, 1);
566 }
567 }
568 } else {
569 if (!empty($object->fk_project)) {
570 $proj = new Project($db);
571 $proj->fetch($object->fk_project);
572 $morehtmlref.=$proj->getNomUrl();
573 } else {
574 $morehtmlref.='';
575 }
576 }
577}
578*/
579$morehtmlref .= '</div>';
580
581
582dol_banner_tab($object, 'ref', $linkback, 1, 'ref', 'ref', $morehtmlref);
583
584
585print '<div class="fichecenter">';
586print '<div class="fichehalfleft">';
587print '<div class="underbanner clearboth"></div>';
588print '<table class="border centpercent tableforfield">'."\n";
589
590// Common attributes
591include DOL_DOCUMENT_ROOT.'/core/tpl/commonfields_view.tpl.php';
592
593// Other attributes. Fields from hook formObjectOptions and Extrafields.
594include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_view.tpl.php';
595
596//print '<tr><td class="titlefield fieldname_invcode">'.$langs->trans("InventoryCode").'</td><td>INV'.$object->id.'</td></tr>';
597
598print '</table>';
599print '</div>';
600print '</div>';
601
602print '<div class="clearboth"></div>';
603
604print dol_get_fiche_end();
605
606print '<form id="formrecord" name="formrecord" method="POST" action="'.$_SERVER["PHP_SELF"].'?page='.$page.'&id='.$object->id.'">';
607print '<input type="hidden" name="token" value="'.newToken().'">';
608print '<input type="hidden" name="action" value="updateinventorylines">';
609print '<input type="hidden" name="id" value="'.$object->id.'">';
610print '<input type="hidden" name="sortfield" value="' . $sortfield . '">';
611print '<input type="hidden" name="sortorder" value="' . $sortorder . '">';
612// Keep the same limit as the displayed page, otherwise the save reads a different page slice
613// (plimit($limit, $offset)) than the one shown and quantities of the extra rows are lost (#35207).
614print '<input type="hidden" name="limit" value="' . ((int) $limit) . '">';
615if ($backtopage) {
616 print '<input type="hidden" name="backtopage" value="'.$backtopage.'">';
617}
618
619
620// Buttons for actions
621if ($action != 'record') {
622 print '<div class="tabsAction">'."\n";
623 $parameters = array();
624 $reshook = $hookmanager->executeHooks('addMoreActionsButtons', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
625 if ($reshook < 0) {
626 setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
627 }
628
629 if (empty($reshook)) {
630 if ($object->status == Inventory::STATUS_DRAFT) {
631 if ($permissiontoupdatestock) {
632 if (getDolGlobalInt('INVENTORY_INCLUDE_SUB_WAREHOUSE') && !empty($object->fk_warehouse)) {
633 print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=validate&token='.newToken().'">'.$langs->trans("Validate").' ('.$langs->trans("Start").')</a>';
634 } else {
635 print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=confirm_validate&confirm=yes&token='.newToken().'">'.$langs->trans("Validate").' ('.$langs->trans("Start").')</a>';
636 }
637 } else {
638 print '<a class="butActionRefused classfortooltip" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans('Validate').' ('.$langs->trans("Start").')</a>'."\n";
639 }
640 }
641
642 // Save
643 if ($object->status == $object::STATUS_VALIDATED) {
644 if ($permissiontoupdatestock) {
645 print '<a class="butAction classfortooltip" id="idbuttonmakemovementandclose" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=record&page='.$page.$paramwithsearch.'&token='.newToken().'" title="'.dol_escape_htmltag($langs->trans("MakeMovementsAndClose")).'">'.$langs->trans("MakeMovementsAndClose").'</a>'."\n";
646 } else {
647 print '<a class="butActionRefused classfortooltip" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans('MakeMovementsAndClose').'</a>'."\n";
648 }
649
650 if ($permissiontoupdatestock) {
651 print '<a class="butActionDelete" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=confirm_cancel&page='.$page.$paramwithsearch.'&token='.newToken().'">'.$langs->trans("Cancel").'</a>'."\n";
652 }
653 }
654 }
655 print '</div>'."\n";
656
657 if ($object->status != Inventory::STATUS_DRAFT && $object->status != Inventory::STATUS_VALIDATED) {
658 print '<br><br>';
659 }
660}
661
662
663
664if ($object->status == Inventory::STATUS_VALIDATED) {
665 print '<center>';
666 if (!empty($conf->use_javascript_ajax)) {
667 if ($permissiontoupdatestock) {
668 // Link to launch scan tool
669 if (isModEnabled('barcode') || isModEnabled('productbatch')) {
670 print '<a href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=updatebyscaning&token='.currentToken().'" class="marginrightonly paddingright marginleftonly paddingleft">'.img_picto('', 'barcode', 'class="paddingrightonly"').$langs->trans("UpdateByScaning").'</a>';
671 }
672
673 // Link to autofill
674 print '<a id="fillwithexpected" class="marginrightonly paddingright marginleftonly paddingleft" href="#">'.img_picto('', 'autofill', 'class="paddingrightonly"').$langs->trans('AutofillWithExpected').'</a>';
675 print '<script>';
676 print '$( document ).ready(function() {';
677 print ' $("#fillwithexpected").on("click",function fillWithExpected(){
678 $(".expectedqty").each(function(){
679 var object = $(this)[0];
680 var objecttofill = $("#"+object.id+"_input")[0];
681 objecttofill.value = object.innerText;
682 jQuery(".realqty").trigger("change");
683 })
684 console.log("Values filled (after click on fillwithexpected)");
685 /* disablebuttonmakemovementandclose(); */
686 return false;
687 });';
688 print '});';
689 print '</script>';
690
691 // Link to reset qty
692 print '<a href="#" id="clearqty" class="marginrightonly paddingright marginleftonly paddingleft">'.img_picto('', 'eraser', 'class="paddingrightonly"').$langs->trans("ClearQtys").'</a>';
693 } else {
694 print '<a class="classfortooltip marginrightonly paddingright marginleftonly paddingleft" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans("Save").'</a>'."\n";
695 }
696 }
697 print '<br>';
698 print '<br>';
699 print '</center>';
700}
701
702
703// Popup for mass barcode scanning
704if ($action == 'updatebyscaning') {
705 if ($permissiontoupdatestock) {
706 // Output the javascript to manage the scanner tool.
707 print '<script>';
708
709 print '
710 var duplicatedbatchcode = [];
711 var errortab1 = [];
712 var errortab2 = [];
713 var errortab3 = [];
714 var errortab4 = [];
715
716 function barcodescannerjs(){
717 console.log("We catch inputs in scanner box");
718 jQuery("#scantoolmessage").text();
719
720 var selectaddorreplace = $("select[name=selectaddorreplace]").val();
721 var barcodemode = $("input[name=barcodemode]:checked").val();
722 var barcodeproductqty = $("input[name=barcodeproductqty]").val();
723 var textarea = $("textarea[name=barcodelist]").val();
724 var textarray = textarea.split(/[\s,;]+/);
725 var tabproduct = [];
726 duplicatedbatchcode = [];
727 errortab1 = [];
728 errortab2 = [];
729 errortab3 = [];
730 errortab4 = [];
731
732 textarray = textarray.filter(function(value){
733 return value != "";
734 });
735 if(textarray.some((element) => element != "")){
736 $(".expectedqty").each(function(){
737 id = this.id;
738 console.log("Analyze the line "+id+" in inventory, barcodemode="+barcodemode);
739 warehouse = $("#"+id+"_warehouse").attr(\'data-ref\');
740 //console.log(warehouse);
741 productbarcode = $("#"+id+"_product").attr(\'data-barcode\');
742 //console.log(productbarcode);
743 productbatchcode = $("#"+id+"_batch").attr(\'data-batch\');
744 //console.log(productbatchcode);
745
746 if (barcodemode != "barcodeforproduct") {
747 tabproduct.forEach(product=>{
748 console.log("product.Batch="+product.Batch+" productbatchcode="+productbatchcode);
749 if(product.Batch != "" && product.Batch == productbatchcode){
750 console.log("duplicate batch code found for batch code "+productbatchcode);
751 duplicatedbatchcode.push(productbatchcode);
752 }
753 })
754 }
755 productinput = $("#"+id+"_input").val();
756 if(productinput == ""){
757 productinput = 0
758 }
759 tabproduct.push({\'Id\':id,\'Warehouse\':warehouse,\'Barcode\':productbarcode,\'Batch\':productbatchcode,\'Qty\':productinput,\'fetched\':false});
760 });
761
762 console.log("Loop on each record entered in the textarea");
763 textarray.forEach(function(element,index){
764 console.log("Process record element="+element+" id="+id);
765 var verify_batch = false;
766 var verify_barcode = false;
767 switch(barcodemode){
768 case "barcodeforautodetect":
769 verify_barcode = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"barcode",true);
770 verify_batch = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"lotserial",true);
771 break;
772 case "barcodeforproduct":
773 verify_barcode = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"barcode");
774 break;
775 case "barcodeforlotserial":
776 verify_batch = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"lotserial");
777 break;
778 default:
779 alert(\''.dol_escape_js($langs->trans("ErrorWrongBarcodemode")).' "\'+barcodemode+\'"\');
780 throw \''.dol_escape_js($langs->trans('ErrorWrongBarcodemode')).' "\'+barcodemode+\'"\';
781 }
782
783 if (verify_batch == false && verify_barcode == false) { /* If the 2 flags are false, not found error */
784 errortab2.push(element);
785 } else if (verify_batch == true && verify_barcode == true) { /* If the 2 flags are true, error: we don t know which one to take */
786 errortab3.push(element);
787 } else if (verify_batch == true) {
788 console.log("element="+element);
789 console.log(duplicatedbatchcode);
790 if (duplicatedbatchcode.includes(element)) {
791 errortab1.push(element);
792 }
793 }
794 });
795
796 if (Object.keys(errortab1).length < 1 && Object.keys(errortab2).length < 1 && Object.keys(errortab3).length < 1) {
797 tabproduct.forEach(product => {
798 if(product.Qty!=0){
799 console.log("We change #"+product.Id+"_input to match input in scanner box");
800 if(product.hasOwnProperty("reelqty")){
801 $.ajax({ url: \''.DOL_URL_ROOT.'/product/inventory/ajax/searchfrombarcode.php\',
802 data: { "token":"'.newToken().'", "action":"addnewlineproduct", "fk_entrepot":product.Warehouse, "batch":product.Batch, "fk_inventory":'.dol_escape_js((string) $object->id).', "fk_product":product.fk_product, "reelqty":product.reelqty},
803 type: \'POST\',
804 async: false,
805 success: function(response) {
806 if (typeof response == "object") {
807 console.log("response is already type object, no need to parse it");
808 } else {
809 console.log("response is type "+(typeof response));
810 response = JSON.parse(response);
811 }
812 if(response.status == "success"){
813 console.log(response.message);
814 $("<input type=\'text\' value=\'"+product.Qty+"\' />")
815 .attr("id", "id_"+response.id_line+"_input")
816 .attr("name", "id_"+response.id_line)
817 .appendTo("#formrecord");
818 }else{
819 console.error(response.message);
820 }
821 },
822 error : function(output) {
823 console.error("Error on line creation function");
824 },
825 });
826 } else {
827 $("#"+product.Id+"_input").val(product.Qty);
828 }
829 }
830 });
831 jQuery("#scantoolmessage").text("'.dol_escape_js($langs->transnoentities("QtyWasAddedToTheScannedBarcode")).'\n");
832 /* document.forms["formrecord"].submit(); */
833 } else {
834 let stringerror = "";
835 if (Object.keys(errortab1).length > 0) {
836 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorSameBatchNumber')).': ";
837 errortab1.forEach(element => {
838 stringerror += (element + ", ")
839 });
840 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
841 }
842 if (Object.keys(errortab2).length > 0) {
843 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorCantFindCodeInInventory')).': ";
844 errortab2.forEach(element => {
845 stringerror += (element + ", ")
846 });
847 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
848 }
849 if (Object.keys(errortab3).length > 0) {
850 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorCodeScannedIsBothProductAndSerial')).': ";
851 errortab3.forEach(element => {
852 stringerror += (element + ", ")
853 });
854 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
855 }
856 if (Object.keys(errortab4).length > 0) {
857 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorBarcodeNotFoundForProductWarehouse')).': ";
858 errortab4.forEach(element => {
859 stringerror += (element + ", ")
860 });
861 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
862 }
863
864 jQuery("#scantoolmessage").html(\''.dol_escape_js($langs->transnoentities("ErrorOnElementsInventory")).'\' + stringerror);
865 //alert("'.dol_escape_js($langs->trans("ErrorOnElementsInventory")).' :\n" + stringerror);
866 }
867 }
868
869 }
870
871 /* This methode is called by parent barcodescannerjs() */
872 function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,mode,autodetect=false){
873 BarcodeIsInProduct=0;
874 newproductrow=0
875 result=false;
876 tabproduct.forEach(product => {
877 $.ajax({ url: \''.DOL_URL_ROOT.'/product/inventory/ajax/searchfrombarcode.php\',
878 data: { "token":"'.newToken().'", "action":"existbarcode", '.(!empty($object->fk_warehouse) ? '"fk_entrepot":'.$object->fk_warehouse.', ' : '').(!empty($object->fk_product) ? '"fk_product":'.$object->fk_product.', ' : '').'"barcode":element, "product":product, "mode":mode},
879 type: \'POST\',
880 async: false,
881 success: function(response) {
882 if (typeof response == "object") {
883 console.log("response is already type object, no need to parse it");
884 } else {
885 console.log("response is type "+(typeof response));
886 response = JSON.parse(response);
887 }
888 if (response.status == "success"){
889 console.log(response.message);
890 if(!newproductrow){
891 newproductrow = response.object;
892 }
893 }else{
894 if (mode!="lotserial" && autodetect==false && !errortab4.includes(element)){
895 errortab4.push(element);
896 console.error(response.message);
897 }
898 }
899 },
900 error : function(output) {
901 console.error("Error on barcodeserialforproduct function");
902 },
903 });
904 console.log("Product "+(index+=1)+": "+element);
905 if(mode == "barcode"){
906 testonproduct = product.Barcode
907 }else if (mode == "lotserial"){
908 testonproduct = product.Batch
909 }
910 if(testonproduct == element){
911 if(selectaddorreplace == "add"){
912 productqty = parseInt(product.Qty,10);
913 product.Qty = productqty + parseInt(barcodeproductqty,10);
914 }else if(selectaddorreplace == "replace"){
915 if(product.fetched == false){
916 product.Qty = barcodeproductqty
917 product.fetched=true
918 }else{
919 productqty = parseInt(product.Qty,10);
920 product.Qty = productqty + parseInt(barcodeproductqty,10);
921 }
922 }
923 BarcodeIsInProduct+=1;
924 }
925 })
926 if(BarcodeIsInProduct==0 && newproductrow!=0){
927 tabproduct.push({\'Id\':tabproduct.length-1,\'Warehouse\':newproductrow.fk_warehouse,\'Barcode\':mode=="barcode"?element:null,\'Batch\':mode=="lotserial"?element:null,\'Qty\':barcodeproductqty,\'fetched\':true,\'reelqty\':newproductrow.reelqty,\'fk_product\':newproductrow.fk_product,\'mode\':mode});
928 result = true;
929 }
930 if(BarcodeIsInProduct > 0){
931 result = true;
932 }
933 return result;
934 }
935 ';
936 print '</script>';
937 }
938 include DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
939 $formother = new FormOther($db);
940 print $formother->getHTMLScannerForm("barcodescannerjs", 'all');
941}
942
943//Call method to undo changes in real qty
944print '<script>';
945print 'jQuery(document).ready(function() {
946 $("#clearqty").on("click", function() {
947 console.log("Clear all values");
948 /* disablebuttonmakemovementandclose(); */
949 jQuery(".realqty").val("");
950 jQuery(".realqty").trigger("change");
951 return false; /* disable submit */
952 });
953 $(".undochangesqty").on("click", function undochangesqty() {
954 console.log("Clear value of inventory line");
955 id = this.id;
956 id = id.split("_")[1];
957 tmpvalue = $("#id_"+id+"_input_tmp").val()
958 $("#id_"+id+"_input")[0].value = tmpvalue;
959 /* disablebuttonmakemovementandclose(); */
960 return false; /* disable submit */
961 });
962});';
963print '</script>';
964
965print '<div class="fichecenter">';
966//print '<div class="fichehalfleft">';
967print '<div class="clearboth"></div>';
968
969//print load_fiche_titre($langs->trans('Consumption'), '', '');
970
971print '<div class="div-table-responsive-no-min">';
972print '<table id="tablelines" class="noborder noshadow centpercent">';
973
974print '<tr class="liste_titre">';
975print getTitleFieldOfList($langs->trans("Warehouse"), 0, $_SERVER['PHP_SELF'], 'e.ref', '', 'id=' . $object->id . '&page=' . $page . $param, '', $sortfield, $sortorder, '', 0, '') . "\n";
976print getTitleFieldOfList($langs->trans("Product"), 0, $_SERVER['PHP_SELF'], 'p.ref', '', 'id=' . $object->id . '&page=' . $page . $param, '', $sortfield, $sortorder, '', 0, '') . "\n";
977if (isModEnabled('productbatch')) {
978 print '<td>';
979 print $langs->trans("Batch");
980 print '</td>';
981}
982if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
983 // Expected quantity = If inventory is open: Quantity currently in stock (may change if stock movement are done during the inventory)
984 print '<td class="right">'.$form->textwithpicto($langs->trans("ExpectedQty"), $langs->trans("QtyCurrentlyKnownInStock")).'</td>';
985} else {
986 // Expected quantity = If inventory is closed: Quantity we had in stock when we start the inventory.
987 print '<td class="right">'.$form->textwithpicto($langs->trans("ExpectedQty"), $langs->trans("QtyInStockWhenInventoryWasValidated")).'</td>';
988}
989if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
990 print '<td class="right">'.$langs->trans('PMPExpected').'</td>';
991 print '<td class="right">'.$langs->trans('ExpectedValuation').'</td>';
992 print '<td class="right">'.$form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp")).'</td>';
993 print '<td class="right">'.$langs->trans('PMPReal').'</td>';
994 print '<td class="right">'.$langs->trans('RealValuation').'</td>';
995} else {
996 print '<td class="right">';
997 print $form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp"));
998 print '</td>';
999}
1000if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1001 // Actions or link to stock movement
1002 print '<td class="center">';
1003 print '</td>';
1004} else {
1005 // Actions or link to stock movement
1006 print '<td class="right">';
1007 //print $langs->trans("StockMovement");
1008 print '</td>';
1009}
1010print '</tr>';
1011
1012// Line to add a new line in inventory
1013if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1014 print '<tr>';
1015 print '<td>';
1016 print $formproduct->selectWarehouses((GETPOSTISSET('fk_warehouse') ? GETPOSTINT('fk_warehouse') : $object->fk_warehouse), 'fk_warehouse', 'warehouseopen', 1, 0, 0, '', 0, 0, array(), 'maxwidth300');
1017 print '</td>';
1018 print '<td>';
1019 if (getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
1020 $filtertype = '';
1021 } else {
1022 $filtertype = 0;
1023 }
1024 print $form->select_produits((GETPOSTISSET('fk_product') ? GETPOSTINT('fk_product') : $object->fk_product), 'fk_product', $filtertype, 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'maxwidth300');
1025 print '</td>';
1026 if (isModEnabled('productbatch')) {
1027 print '<td>';
1028 print '<input type="text" name="batch" class="maxwidth100" value="'.(GETPOSTISSET('batch') ? GETPOST('batch') : '').'">';
1029 print '</td>';
1030 }
1031 print '<td class="right"></td>';
1032 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1033 print '<td class="right">';
1034 print '</td>';
1035 print '<td class="right">';
1036 print '</td>';
1037 print '<td class="right">';
1038 print '<input type="text" name="qtytoadd" class="maxwidth75" value="">';
1039 print '</td>';
1040 print '<td class="right">';
1041 print '</td>';
1042 print '<td class="right">';
1043 print '</td>';
1044 } else {
1045 print '<td class="right">';
1046 print '<input type="text" name="qtytoadd" class="maxwidth75" value="">';
1047 print '</td>';
1048 }
1049 // Actions
1050 print '<td class="center">';
1051 if ($permissiontoupdatestock) {
1052 print '<input type="submit" class="button paddingright" name="addline" value="'.$langs->trans("Add").'">';
1053 } else {
1054 print '<input type="submit" class="button paddingright" disabled="disabled" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'" name="addline" value="'.$langs->trans("Add").'">';
1055 }
1056 print '</td>';
1057 print '</tr>';
1058}
1059
1060// Request to show lines of inventory (prefilled after start/validate step)
1061$sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
1062$sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated, id.fk_movement, id.pmp_real, id.pmp_expected';
1063$sql .= ' FROM ' . $db->prefix() . 'inventorydet as id';
1064$sql .= ' LEFT JOIN ' . $db->prefix() . 'product as p ON id.fk_product = p.rowid';
1065$sql .= ' LEFT JOIN ' . $db->prefix() . 'entrepot as e ON id.fk_warehouse = e.rowid';
1066$sql .= ' WHERE id.fk_inventory = ' . ((int) $object->id);
1067$sql .= $db->order($sortfield, $sortorder);
1068$sql .= $db->plimit($limit, $offset);
1069
1070$cacheOfProducts = array();
1071$cacheOfWarehouses = array();
1072
1073//$sql = '';
1074$resql = $db->query($sql);
1075if ($resql) {
1076 $num = $db->num_rows($resql);
1077
1078 if (!empty($limit != 0) || $num > $limit || $page) {
1079 print_fleche_navigation($page, $_SERVER["PHP_SELF"], '&id='.$object->id.$paramwithsearch, ($num >= $limit ? 1 : 0), '<li class="pagination"><span>' . $langs->trans("Page") . ' ' . ($page + 1) . '</span></li>', '', $limit);
1080 }
1081
1082 $i = 0;
1083 $hasinput = false;
1084 $totalarray = array();
1085 while ($i < $num) {
1086 $obj = $db->fetch_object($resql);
1087
1088 if (isset($cacheOfWarehouses[$obj->fk_warehouse])) {
1089 $warehouse_static = $cacheOfWarehouses[$obj->fk_warehouse];
1090 } else {
1091 $warehouse_static = new Entrepot($db);
1092 $warehouse_static->fetch($obj->fk_warehouse);
1093
1094 $cacheOfWarehouses[$warehouse_static->id] = $warehouse_static;
1095 }
1096
1097 // Load real stock we have now
1098 $option = '';
1099 if (isset($cacheOfProducts[$obj->fk_product])) {
1100 $product_static = $cacheOfProducts[$obj->fk_product];
1101 } else {
1102 $product_static = new Product($db);
1103 $result = $product_static->fetch($obj->fk_product, '', '', '', 1, 1, 1);
1104
1105 //$option = 'nobatch';
1106 $option .= ',novirtual';
1107 $product_static->load_stock($option); // Load stock_reel + stock_warehouse.
1108
1109 $cacheOfProducts[$product_static->id] = $product_static;
1110 }
1111
1112 print '<tr class="oddeven">';
1113 print '<td id="id_'.$obj->rowid.'_warehouse" data-ref="'.dol_escape_htmltag($warehouse_static->ref).'">';
1114 print $warehouse_static->getNomUrl(1);
1115 print '</td>';
1116 print '<td id="id_'.$obj->rowid.'_product" data-ref="'.dol_escape_htmltag($product_static->ref).'" data-barcode="'.dol_escape_htmltag($product_static->barcode).'">';
1117 print $product_static->getNomUrl(1).' - '.$product_static->label;
1118 print '</td>';
1119
1120 if (isModEnabled('productbatch')) {
1121 print '<td id="id_'.$obj->rowid.'_batch" data-batch="'.dol_escape_htmltag($obj->batch).'">';
1122 $batch_static = new Productlot($db);
1123 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1124 $res = $batch_static->fetch(0, $product_static->id, $obj->batch);
1125 if ($res) {
1126 print $batch_static->getNomUrl(1);
1127 } else {
1128 print dol_escape_htmltag($obj->batch);
1129 }
1130 print '</td>';
1131 }
1132
1133 // Expected quantity = If inventory is open: Quantity currently in stock (may change if stock movement are done during the inventory)
1134 // Expected quantity = If inventory is closed: Quantity we had in stock when we start the inventory.
1135 print '<td class="right expectedqty" id="id_'.$obj->rowid.'" title="Stock viewed at last update: '.$obj->qty_stock.'">';
1136 $valuetoshow = $obj->qty_stock;
1137 // For inventory not yet close, we overwrite with the real value in stock now
1138 if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1139 if (isModEnabled('productbatch') && $product_static->hasbatch()) {
1140 $valuetoshow = $product_static->stock_warehouse[$obj->fk_warehouse]->detail_batch[$obj->batch]->qty ?? 0;
1141 } else {
1142 $valuetoshow = !empty($product_static->stock_warehouse[$obj->fk_warehouse]->real) ? $product_static->stock_warehouse[$obj->fk_warehouse]->real : 0;
1143 }
1144 }
1145 print price2num($valuetoshow, 'MS');
1146 print '<input type="hidden" name="stock_qty_'.$obj->rowid.'" value="'.$valuetoshow.'">';
1147 print '</td>';
1148
1149 // Real quantity
1150 if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1151 $qty_view = GETPOST("id_".$obj->rowid) && price2num(GETPOST("id_".$obj->rowid), 'MS') >= 0 ? GETPOST("id_".$obj->rowid) : $obj->qty_view;
1152
1153 //if (!$hasinput && $qty_view !== null && $obj->qty_stock != $qty_view) {
1154 if ($qty_view != '') {
1155 $hasinput = true;
1156 }
1157
1158 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1159 //PMP Expected
1160 if (!empty($obj->pmp_expected)) {
1161 $pmp_expected = $obj->pmp_expected;
1162 } else {
1163 $pmp_expected = $product_static->pmp;
1164 }
1165 $pmp_valuation = $pmp_expected * $valuetoshow;
1166 print '<td class="right">';
1167 print is_null($pmp_expected) ? '' : price($pmp_expected);
1168 print '<input type="hidden" name="expectedpmp_'.$obj->rowid.'" value="'.$pmp_expected.'"/>';
1169 print '</td>';
1170 print '<td class="right">';
1171 print price($pmp_valuation);
1172 print '</td>';
1173
1174 print '<td class="right">';
1175 print '<a id="undochangesqty_'.$obj->rowid.'" href="#" class="undochangesqty reposition marginrightonly" title="'.dol_escape_htmltag($langs->trans("Clear")).'">';
1176 print img_picto('', 'eraser', 'class="opacitymedium"');
1177 print '</a>';
1178 print '<input type="text" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input" value="'.$qty_view.'">';
1179 print '</td>';
1180
1181 //PMP Real
1182 print '<td class="right">';
1183 if (!empty($obj->pmp_real) || (string) $obj->pmp_real === '0') {
1184 $pmp_real = $obj->pmp_real;
1185 } else {
1186 $pmp_real = $product_static->pmp;
1187 }
1188 $pmp_valuation_real = $pmp_real * $qty_view;
1189 print '<input type="text" class="maxwidth75 right realpmp'.$obj->fk_product.'" name="realpmp_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input_pmp" value="'.(is_null($pmp_real) ? '' : price2num($pmp_real)).'">';
1190 print '</td>';
1191 print '<td class="right">';
1192 print '<input type="text" class="maxwidth75 right realvaluation'.$obj->fk_product.'" name="realvaluation_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input_real_valuation" value="'.$pmp_valuation_real.'">';
1193 print '</td>';
1194
1195 $totalExpectedValuation += $pmp_valuation;
1196 $totalRealValuation += $pmp_valuation_real;
1197 } else {
1198 print '<td class="right">';
1199 print '<a id="undochangesqty_'.$obj->rowid.'" href="#" class="undochangesqty reposition marginrightonly" title="'.dol_escape_htmltag($langs->trans("Clear")).'">';
1200 print img_picto('', 'eraser', 'class="opacitymedium"');
1201 print '</a>';
1202 print '<input type="text" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input" value="'.$qty_view.'">';
1203 print '</td>';
1204 }
1205
1206 // Picto delete line
1207 print '<td class="right">';
1208 if ($permissiontoupdatestock) {
1209 print '<a class="reposition" href="'.DOL_URL_ROOT.'/product/inventory/inventory.php?id='.$object->id.'&lineid='.$obj->rowid.'&action=deleteline&page='.$page.$paramwithsearch.'&token='.newToken().'">'.img_delete().'</a>';
1210 }
1211 $qty_tmp = price2num(GETPOST("id_".$obj->rowid."_input_tmp"), 'MS') >= 0 ? GETPOST("id_".$obj->rowid."_input_tmp") : $qty_view;
1212 print '<input type="hidden" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'_input_tmp" id="id_'.$obj->rowid.'_input_tmp" value="'.$qty_tmp.'">';
1213 print '</td>';
1214 } else {
1215 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1216 // PMP Expected
1217 if (!empty($obj->pmp_expected)) {
1218 $pmp_expected = $obj->pmp_expected;
1219 } else {
1220 $pmp_expected = $product_static->pmp;
1221 }
1222 $pmp_valuation = $pmp_expected * $valuetoshow;
1223 print '<td class="right">';
1224 print is_null($pmp_expected) ? '' : price($pmp_expected);
1225 print '</td>';
1226 print '<td class="right">';
1227 print price($pmp_valuation);
1228 print '</td>';
1229
1230 print '<td class="right nowraponall">';
1231 print $obj->qty_view; // qty found
1232 print '</td>';
1233
1234 // PMP Real
1235 print '<td class="right">';
1236 if (!empty($obj->pmp_real)) {
1237 $pmp_real = $obj->pmp_real;
1238 } else {
1239 $pmp_real = $product_static->pmp;
1240 }
1241 $pmp_valuation_real = $pmp_real * $obj->qty_view;
1242 print is_null($pmp_real) ? '' : price($pmp_real);
1243 print '</td>';
1244 print '<td class="right">';
1245 print price($pmp_valuation_real);
1246 print '</td>';
1247 print '<td class="nowraponall right">';
1248
1249 $totalExpectedValuation += $pmp_valuation;
1250 $totalRealValuation += $pmp_valuation_real;
1251 } else {
1252 print '<td class="right nowraponall">';
1253 print $obj->qty_view; // qty found
1254 print '</td>';
1255 }
1256 print '<td>';
1257 if ($obj->fk_movement > 0) {
1258 $stockmovment = new MouvementStock($db);
1259 $stockmovment->fetch($obj->fk_movement);
1260 print $stockmovment->getNomUrl(1, 'movements');
1261 }
1262 print '</td>';
1263 }
1264 print '</tr>';
1265
1266 $i++;
1267 }
1268} else {
1269 dol_print_error($db);
1270}
1271if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1272 print '<tr class="liste_total">';
1273 print '<td colspan="4">'.$langs->trans("Total").'</td>';
1274 print '<td class="right" colspan="2">'.price($totalExpectedValuation).'</td>';
1275 print '<td class="right" id="totalRealValuation" colspan="3">'.price($totalRealValuation).'</td>';
1276 print '<td></td>';
1277 print '</tr>';
1278}
1279print '</table>';
1280
1281print '</div>';
1282
1283if ($object->status == $object::STATUS_VALIDATED) {
1284 print '<center><input id="submitrecord" type="submit" class="button button-save" name="save" value="'.$langs->trans("Save").'"></center>';
1285}
1286
1287print '</div>';
1288
1289
1290// Call method to disable the button if no qty entered yet for inventory
1291/*
1292if ($object->status != $object::STATUS_VALIDATED || !$hasinput) {
1293 print '<script type="text/javascript">
1294 jQuery(document).ready(function() {
1295 console.log("Call disablebuttonmakemovementandclose because status = '.((int) $object->status).' or $hasinput = '.((int) $hasinput).'");
1296 disablebuttonmakemovementandclose();
1297 });
1298 </script>';
1299}
1300*/
1301
1302print '</form>';
1303
1304print '<script type="text/javascript">
1305 $(document).ready(function() {
1306
1307 $(".paginationnext:last").click(function(e){
1308 var form = $("#formrecord");
1309 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1310 $.ajax({
1311 url: actionURL,
1312 data: form.serialize(),
1313 cache: false,
1314 success: function(result){
1315 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page + 1).$paramwithsearch.'";
1316 }});
1317 return false;
1318 });
1319
1320
1321 $(".paginationprevious:last").click(function(e){
1322 var form = $("#formrecord");
1323 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1324 $.ajax({
1325 url: actionURL,
1326 data: form.serialize(),
1327 cache: false,
1328 success: function(result){
1329 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page - 1).$paramwithsearch.'";
1330 }});
1331 return false;
1332 });
1333
1334 $("#idbuttonmakemovementandclose").click(function(e){
1335 var form = $("#formrecord");
1336 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1337 $.ajax({
1338 url: actionURL,
1339 type: "POST",
1340 data: form.serialize(),
1341 cache: false,
1342 success: function(result){
1343 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'&action=record";
1344 }});
1345 return false;
1346 });
1347 });
1348</script>';
1349
1350
1351if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1352 ?>
1353<script type="text/javascript">
1354$('.realqty').on('change', function () {
1355 let realqty = $(this).closest('tr').find('.realqty').val();
1356 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1357 let realpmp = $(inputPmp).val();
1358 if (!isNaN(realqty) && !isNaN(realpmp)) {
1359 let realval = realqty * realpmp;
1360 $(this).closest('tr').find('input[name^=realvaluation]').val(realval.toFixed(2));
1361 }
1362 updateTotalValuation();
1363});
1364
1365$('input[class*=realpmp]').on('change', function () {
1366 let inputQtyReal = $(this).closest('tr').find('.realqty');
1367 let realqty = $(inputQtyReal).val();
1368 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1369 console.log(inputPmp);
1370 let realPmpClassname = $(inputPmp).attr('class').match(/[\w-]*realpmp[\w-]*/g)[0];
1371 let realpmp = $(inputPmp).val();
1372 if (!isNaN(realpmp)) {
1373 $('.'+realPmpClassname).val(realpmp); //For batch case if pmp is changed we change it everywhere it's same product and calc back everything
1374
1375 if (!isNaN(realqty)) {
1376 let realval = realqty * realpmp;
1377 $(this).closest('tr').find('input[name^=realvaluation]').val(realval.toFixed(2));
1378 }
1379 $('.realqty').trigger('change');
1380 updateTotalValuation();
1381 }
1382});
1383
1384$('input[name^=realvaluation]').on('change', function () {
1385 let inputQtyReal = $(this).closest('tr').find('.realqty');
1386 let realqty = $(inputQtyReal).val();
1387 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1388 let inputRealValuation = $(this).closest('tr').find('input[name^=realvaluation]');
1389 let realPmpClassname = $(inputPmp).attr('class').match(/[\w-]*realpmp[\w-]*/g)[0];
1390 let realvaluation = $(inputRealValuation).val();
1391 if (!isNaN(realvaluation) && !isNaN(realqty) && realvaluation !== '' && realqty !== '' && realqty !== 0) {
1392 let realpmp = realvaluation / realqty
1393 $('.'+realPmpClassname).val(realpmp); //For batch case if pmp is changed we change it everywhere it's same product and calc back everything
1394 $('.realqty').trigger('change');
1395 updateTotalValuation();
1396 }
1397});
1398
1399function updateTotalValuation() {
1400 let total = 0;
1401 $('input[name^=realvaluation]').each(function( index ) {
1402 let val = $(this).val();
1403 if(!isNaN(val)) total += parseFloat($(this).val());
1404 });
1405 let currencyFractionDigits = new Intl.NumberFormat('fr-FR', {
1406 style: 'currency',
1407 currency: 'EUR',
1408 }).resolvedOptions().maximumFractionDigits;
1409 $('#totalRealValuation').html(total.toLocaleString('fr-FR', {
1410 maximumFractionDigits: currencyFractionDigits
1411 }));
1412}
1413
1414
1415</script>
1416 <?php
1417}
1418
1419// End of page
1420llxFooter();
1421$db->close();
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:48
if( $user->socid > 0) if(! $user->hasRight('accounting', 'chartofaccount')) $object
Definition card.php:67
$totalarray
Definition export.php:1206
llxFooter($comment='', $zone='private', $disabledoutputofmessages=0)
Empty footer.
Definition wrapper.php:91
if(!defined('NOREQUIRESOC')) if(!defined( 'NOREQUIRETRAN')) if(!defined('NOTOKENRENEWAL')) if(!defined( 'NOREQUIREMENU')) if(!defined('NOREQUIREHTML')) if(!defined( 'NOREQUIREAJAX')) llxHeader($head='', $title='', $help_url='', $target='', $disablejs=0, $disablehead=0, $arrayofjs='', $arrayofcss='', $morequerystring='', $morecssonbody='', $replacemainareaby='', $disablenofollow=0, $disablenoindex=0)
Empty header.
Definition wrapper.php:73
Class to manage warehouses.
Class to manage standard extra fields.
Class to manage generation of HTML components Only common components must be here.
Class permettant la generation de composants html autre Only common components are here.
Class with static methods for building HTML components related to products Only components common to ...
Class for Inventory.
Class InventoryLine.
Class to manage stock movements.
Class to manage products or services.
Class with list of lots and properties.
setEventMessages($mesg, $mesgs, $style='mesgs', $messagekey='', $noduplicate=0, $attop=0)
Set event messages in dol_events session object.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
img_delete($titlealt='default', $other='class="pictodelete"', $morecss='')
Show delete logo.
GETPOSTINT($paramname, $method=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
dol_get_fiche_head($links=array(), $active='', $title='', $notab=0, $picto='', $pictoisfullpath=0, $morehtmlright='', $morecss='', $limittoshow=0, $moretabssuffix='', $dragdropfile=0, $morecssdiv='')
Show tabs of a record.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
currentToken()
Return the value of token currently saved into session with name 'token'.
dol_get_fiche_end($notab=0)
Return tab footer of a card.
price($amount, $form=0, $outlangs='', $trunc=1, $rounding=-1, $forcerounding=-1, $currency_code='')
Function to format a value into an amount for visual output Function used into PDF and HTML pages.
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_escape_js($stringtoescape, $mode=0, $noescapebackslashn=0)
Returns text escaped for inclusion into javascript code.
newToken()
Return the value of token currently saved into session with name 'newtoken'.
getTitleFieldOfList($name, $thead=0, $file="", $field="", $begin="", $moreparam="", $moreattrib="", $sortfield="", $sortorder="", $prefix="", $disablesortlink=0, $tooltip='', $forcenowrapcolumntitle=0)
Get title line of an array.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0)
Return value of a param into GET or POST supervariable.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
print_fleche_navigation($page, $file, $options='', $nextpage=0, $betweenarrows='', $afterarrows='', $limit=-1, $totalnboflines=0, $selectlimitsuffix='', $beforearrows='', $hidenavigation=0)
Function to show navigation arrows into lists.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
inventoryPrepareHead(&$inventory, $title='Inventory', $get='')
Define head array for tabs of inventory tools setup pages.
global $conf
The following vars must be defined: $type2label $form $conf, $lang, The following vars may also be de...
Definition member.php:79
if(preg_match('/(crypted|dolcrypt):/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
Definition repair.php:158
restrictedArea(User $user, $features, $object=0, $tableandshare='', $feature2='', $dbt_keyfield='fk_soc', $dbt_select='rowid', $isdraft=0, $mode=0)
Check permissions of a user to show a page and an object.