dolibarr 24.0.1
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-2025 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2025-2026 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//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 // Mirror Product::fetch (product.class.php:2995-2997) which reads pmp from
265 // llx_product_perentity only when MULTICOMPANY_PRODUCT_SHARING_ENABLED and
266 // MULTICOMPANY_PMP_PER_ENTITY_ENABLED are both set. MAIN_PRODUCT_PERENTITY_SHARED
267 // is the accountancy-codes flag; using it to gate the pmp write here means we
268 // silently write into a row that fetch never looks at (#37773).
269 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED') && getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
270 $sqlpmp = 'UPDATE '.MAIN_DB_PREFIX.'product_perentity SET pmp = '.((float) $line->pmp_real).' WHERE fk_product = '.((int) $line->fk_product).' AND entity='.((int) $conf->entity);
271 $resqlpmp = $db->query($sqlpmp);
272 if (! $resqlpmp) {
273 $error++;
274 setEventMessages($db->lasterror(), null, 'errors');
275 break;
276 }
277 }
278 }
279 }
280 $i++;
281 }
282
283 if (!$error) {
284 $object->setRecorded($user);
285 }
286 } else {
287 setEventMessages($db->lasterror, null, 'errors');
288 $error++;
289 }
290
291 if (! $error) {
292 $db->commit();
293 } else {
294 $db->rollback();
295 }
296 $action = '';
297 }
298
299 // Save quantity found during inventory (when we click on Save button on inventory page)
300 if ($action == 'updateinventorylines' && $permissiontoupdatestock) {
301 $sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
302 $sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated';
303 $sql .= ' FROM '.MAIN_DB_PREFIX.'inventorydet as id';
304 $sql .= ' LEFT JOIN ' . $db->prefix() . 'product as p ON id.fk_product = p.rowid';
305 $sql .= ' LEFT JOIN ' . $db->prefix() . 'entrepot as e ON id.fk_warehouse = e.rowid';
306 $sql .= ' WHERE id.fk_inventory = '.((int) $object->id);
307 $sql .= $db->order($sortfield, $sortorder);
308 $sql .= $db->plimit($limit, $offset);
309
310 $db->begin();
311
312 $resql = $db->query($sql);
313 if ($resql) {
314 $num = $db->num_rows($resql);
315 $i = 0;
316 $totalarray = array();
317 $inventoryline = new InventoryLine($db);
318
319 while ($i < $num) {
320 $line = $db->fetch_object($resql);
321 $lineid = $line->rowid;
322
323 $result = 0;
324 $resultupdate = 0;
325
326 if (GETPOST("id_".$lineid, 'alpha') != '') { // If a value was set ('0' or something else)
327 $qtytoupdate = (float) price2num(GETPOST("id_".$lineid, 'alpha'), 'MS');
328 $result = $inventoryline->fetch($lineid);
329 if ($qtytoupdate < 0) {
330 $result = -1;
331 setEventMessages($langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv("RealQty")), null, 'errors');
332 }
333 if ($result > 0) {
334 $inventoryline->qty_stock = (float) price2num(GETPOST('stock_qty_'.$lineid, 'alpha'), 'MS'); // The new value that was set in as hidden field
335 $inventoryline->qty_view = $qtytoupdate; // The new value we want
336 $inventoryline->pmp_real = price2num(GETPOST('realpmp_'.$lineid, 'alpha'), 'MS');
337 $inventoryline->pmp_expected = price2num(GETPOST('expectedpmp_'.$lineid, 'alpha'), 'MS');
338 $resultupdate = $inventoryline->update($user);
339 }
340 } elseif (GETPOSTISSET('id_' . $lineid)) {
341 // Delete record
342 $result = $inventoryline->fetch($lineid);
343 if ($result > 0) {
344 $inventoryline->qty_view = null; // The new value we want
345 $inventoryline->pmp_real = price2num(GETPOST('realpmp_'.$lineid, 'alpha'), 'MS');
346 $inventoryline->pmp_expected = price2num(GETPOST('expectedpmp_'.$lineid, 'alpha'), 'MS');
347 $resultupdate = $inventoryline->update($user);
348 }
349 }
350
351 if ($result < 0 || $resultupdate < 0) {
352 $error++;
353 }
354
355 $i++;
356 }
357 }
358
359 // Update line with id of stock movement (and the start quantity if it has changed this last recording)
360 if (! $error) {
361 $sqlupdate = "UPDATE ".MAIN_DB_PREFIX."inventory";
362 $sqlupdate .= " SET fk_user_modif = ".((int) $user->id);
363 $sqlupdate .= " WHERE rowid = ".((int) $object->id);
364 $resqlupdate = $db->query($sqlupdate);
365 if (! $resqlupdate) {
366 $error++;
367 setEventMessages($db->lasterror(), null, 'errors');
368 }
369 }
370
371 if (!$error) {
372 $db->commit();
373 } else {
374 $db->rollback();
375 }
376 }
377
378 $backurlforlist = DOL_URL_ROOT.'/product/inventory/list.php';
379 $backtopage = DOL_URL_ROOT.'/product/inventory/inventory.php?id='.$object->id.'&page='.$page.$paramwithsearch;
380
381 // Actions cancel, add, update, delete or clone
382 include DOL_DOCUMENT_ROOT.'/core/actions_addupdatedelete.inc.php';
383
384 // Actions when linking object each other
385 include DOL_DOCUMENT_ROOT.'/core/actions_dellink.inc.php';
386
387 // Actions when printing a doc from card
388 include DOL_DOCUMENT_ROOT.'/core/actions_printing.inc.php';
389
390 // Actions to send emails
391 /*$triggersendname = 'MYOBJECT_SENTBYMAIL';
392 $autocopy='MAIN_MAIL_AUTOCOPY_MYOBJECT_TO';
393 $trackid='stockinv'.$object->id;
394 include DOL_DOCUMENT_ROOT.'/core/actions_sendmails.inc.php';*/
395
396 if (GETPOST('addline', 'alpha')) {
397 $qty = (GETPOST('qtytoadd') != '' ? ((float) price2num(GETPOST('qtytoadd'), 'MS')) : null);
398 if ($fk_warehouse <= 0) {
399 $error++;
400 setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Warehouse")), null, 'errors');
401 }
402 if ($fk_product <= 0) {
403 $error++;
404 setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Product")), null, 'errors');
405 }
406 if (price2num(GETPOST('qtytoadd'), 'MS') < 0) {
407 $error++;
408 setEventMessages($langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv("RealQty")), null, 'errors');
409 }
410 if (!$error && isModEnabled('productbatch')) {
411 $tmpproduct = new Product($db);
412 $result = $tmpproduct->fetch($fk_product);
413
414 if (empty($error) && $tmpproduct->status_batch > 0 && empty($batch)) {
415 $error++;
416 $langs->load("errors");
417 setEventMessages($langs->trans("ErrorProductNeedBatchNumber", $tmpproduct->ref), null, 'errors');
418 }
419 if (empty($error) && $tmpproduct->status_batch == 2 && !empty($batch) && $qty > 1) {
420 $error++;
421 $langs->load("errors");
422 setEventMessages($langs->trans("TooManyQtyForSerialNumber", $tmpproduct->ref, $batch), null, 'errors');
423 }
424 if (empty($error) && empty($tmpproduct->status_batch) && !empty($batch)) {
425 $error++;
426 $langs->load("errors");
427 setEventMessages($langs->trans("ErrorProductDoesNotNeedBatchNumber", $tmpproduct->ref), null, 'errors');
428 }
429 }
430 if (!$error) {
431 $tmp = new InventoryLine($db);
432 $tmp->fk_inventory = $object->id;
433 $tmp->fk_warehouse = $fk_warehouse;
434 $tmp->fk_product = $fk_product;
435 $tmp->batch = $batch;
436 $tmp->datec = $now;
437 $tmp->qty_view = $qty;
438
439 $result = $tmp->create($user);
440 if ($result < 0) {
441 if ($db->lasterrno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
442 $langs->load("errors");
443 setEventMessages($langs->trans("ErrorRecordAlreadyExists"), null, 'errors');
444 } else {
445 dol_print_error($db, $tmp->error, $tmp->errors);
446 }
447 } else {
448 // Clear var
449 $_POST['batch'] = ''; // TODO Replace this with a var
450 $_POST['qtytoadd'] = '';
451 }
452 }
453 }
454}
455
456
457
458/*
459 * View
460 */
461
462$form = new Form($db);
463$formproduct = new FormProduct($db);
464
465$help_url = '';
466
467llxHeader('', $langs->trans('Inventory'), $help_url, '', 0, 0, '', '', '', 'mod-product page-inventory_inventory');
468
469// Part to show record
470if ($object->id <= 0) {
471 dol_print_error(null, 'Bad value for object id');
472 exit;
473}
474
475$param = '';
476if ($limit > 0 && $limit != $conf->liste_limit) {
477 $param .= '&limit=' . ((int) $limit);
478}
479
480
481$res = $object->fetch_optionals();
482
483$head = inventoryPrepareHead($object);
484print dol_get_fiche_head($head, 'inventory', $langs->trans("Inventory"), -1, 'stock');
485
486$formconfirm = '';
487
488// Confirmation to delete
489if ($action == 'delete') {
490 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('DeleteInventory'), $langs->trans('ConfirmDeleteOrder'), 'confirm_delete', '', 0, 1);
491}
492// Confirmation to delete line
493if ($action == 'deleteline') {
494 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id.'&lineid='.$lineid.'&page='.$page.$paramwithsearch, $langs->trans('DeleteLine'), $langs->trans('ConfirmDeleteLine'), 'confirm_deleteline', '', 0, 1);
495}
496
497// Clone confirmation
498if ($action == 'clone') {
499 // Create an array for form
500 $formquestion = array();
501 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('ToClone'), $langs->trans('ConfirmCloneMyObject', $object->ref), 'confirm_clone', $formquestion, 'yes', 1);
502}
503
504// Confirmation to close
505if ($action == 'record') {
506 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id.'&page='.$page.$paramwithsearch, $langs->trans('Close'), $langs->trans('ConfirmFinish'), 'update', '', 0, 1);
507 $action = 'view';
508}
509
510// Confirmation to close
511if ($action == 'confirm_cancel') {
512 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('Cancel'), $langs->trans('ConfirmCancel'), 'cancel_record', '', 0, 1);
513 $action = 'view';
514}
515
516if ($action == 'validate') {
517 $form = new Form($db);
518 $formquestion = '';
519 if (getDolGlobalInt('INVENTORY_INCLUDE_SUB_WAREHOUSE') && !empty($object->fk_warehouse)) {
520 $formquestion = array(
521 array('type' => 'checkbox', 'name' => 'include_sub_warehouse', 'label' => $langs->trans("IncludeSubWarehouse"), 'value' => 1, 'size' => '10'),
522 );
523 $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id, $langs->trans('ValidateInventory'), $langs->trans('IncludeSubWarehouseExplanation'), 'confirm_validate', $formquestion, '', 1);
524 }
525}
526
527// Call Hook formConfirm
528$parameters = array('formConfirm' => $formconfirm, 'lineid' => $lineid);
529$reshook = $hookmanager->executeHooks('formConfirm', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
530if (empty($reshook)) {
531 $formconfirm .= $hookmanager->resPrint;
532} elseif ($reshook > 0) {
533 $formconfirm = $hookmanager->resPrint;
534}
535
536// Print form confirm
537print $formconfirm;
538
539
540// Object card
541// ------------------------------------------------------------
542$linkback = '<a href="'.DOL_URL_ROOT.'/product/inventory/list.php">'.$langs->trans("BackToList").'</a>';
543
544$morehtmlref = '<div class="refidno">';
545/*
546// Ref bis
547$morehtmlref.=$form->editfieldkey("RefBis", 'ref_client', $object->ref_client, $object, $user->rights->inventory->creer, 'string', '', 0, 1);
548$morehtmlref.=$form->editfieldval("RefBis", 'ref_client', $object->ref_client, $object, $user->rights->inventory->creer, 'string', '', null, null, '', 1);
549// Thirdparty
550$morehtmlref.='<br>'.$langs->trans('ThirdParty') . ' : ' . $soc->getNomUrl(1);
551// Project
552if (isModEnabled('project'))
553{
554 $langs->load("projects");
555 $morehtmlref.='<br>'.$langs->trans('Project') . ' ';
556 if ($user->rights->inventory->creer)
557 {
558 if ($action != 'classify')
559 {
560 $morehtmlref.='<a class="editfielda" href="' . dolBuildUrl($_SERVER['PHP_SELF'], ['action' => 'classify', 'id' => $object->id], true) . '">' . img_edit($langs->transnoentitiesnoconv('SetProject')) . '</a> : ';
561 if ($action == 'classify') {
562 //$morehtmlref.=$form->form_project($_SERVER['PHP_SELF'] . '?id=' . $object->id, $object->socid, $object->fk_project, 'projectid', 0, 0, 1, 1);
563 $morehtmlref.='<form method="post" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'">';
564 $morehtmlref.='<input type="hidden" name="action" value="classin">';
565 $morehtmlref.='<input type="hidden" name="token" value="'.newToken().'">';
566 $morehtmlref.=$formproject->select_projects($object->socid, $object->fk_project, 'projectid', $maxlength, 0, 1, 0, 1, 0, 0, '', 1);
567 $morehtmlref.='<input type="submit" class="button valignmiddle" value="'.$langs->trans("Modify").'">';
568 $morehtmlref.='</form>';
569 } else {
570 $morehtmlref.=$form->form_project($_SERVER['PHP_SELF'] . '?id=' . $object->id, $object->socid, $object->fk_project, 'none', 0, 0, 0, 1);
571 }
572 }
573 } else {
574 if (!empty($object->fk_project)) {
575 $proj = new Project($db);
576 $proj->fetch($object->fk_project);
577 $morehtmlref.=$proj->getNomUrl();
578 } else {
579 $morehtmlref.='';
580 }
581 }
582}
583*/
584$morehtmlref .= '</div>';
585
586
587dol_banner_tab($object, 'ref', $linkback, 1, 'ref', 'ref', $morehtmlref);
588
589
590print '<div class="fichecenter">';
591print '<div class="fichehalfleft">';
592print '<div class="underbanner clearboth"></div>';
593print '<table class="border centpercent tableforfield">'."\n";
594
595// Common attributes
596include DOL_DOCUMENT_ROOT.'/core/tpl/commonfields_view.tpl.php';
597
598// Other attributes. Fields from hook formObjectOptions and Extrafields.
599include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_view.tpl.php';
600
601//print '<tr><td class="titlefield fieldname_invcode">'.$langs->trans("InventoryCode").'</td><td>INV'.$object->id.'</td></tr>';
602
603print '</table>';
604print '</div>';
605print '</div>';
606
607print '<div class="clearboth"></div>';
608
609print dol_get_fiche_end();
610
611print '<form id="formrecord" name="formrecord" method="POST" action="'.$_SERVER["PHP_SELF"].'?page='.$page.'&id='.$object->id.'">';
612print '<input type="hidden" name="token" value="'.newToken().'">';
613print '<input type="hidden" name="action" value="updateinventorylines">';
614print '<input type="hidden" name="id" value="'.$object->id.'">';
615print '<input type="hidden" name="sortfield" value="' . $sortfield . '">';
616print '<input type="hidden" name="sortorder" value="' . $sortorder . '">';
617// Keep the same limit as the displayed page, otherwise the save reads a different page slice
618// (plimit($limit, $offset)) than the one shown and quantities of the extra rows are lost (#35207).
619print '<input type="hidden" name="limit" value="' . ((int) $limit) . '">';
620if ($backtopage) {
621 print '<input type="hidden" name="backtopage" value="'.$backtopage.'">';
622}
623
624
625// Buttons for actions
626if ($action != 'record') {
627 print '<div class="tabsAction">'."\n";
628 $parameters = array();
629 $reshook = $hookmanager->executeHooks('addMoreActionsButtons', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
630 if ($reshook < 0) {
631 setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
632 }
633
634 if (empty($reshook)) {
635 if ($object->status == Inventory::STATUS_DRAFT) {
636 if ($permissiontoupdatestock) {
637 if (getDolGlobalInt('INVENTORY_INCLUDE_SUB_WAREHOUSE') && !empty($object->fk_warehouse)) {
638 print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=validate&token='.newToken().'">'.$langs->trans("Validate").' ('.$langs->trans("Start").')</a>';
639 } else {
640 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>';
641 }
642 } else {
643 print '<a class="butActionRefused classfortooltip" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans('Validate').' ('.$langs->trans("Start").')</a>'."\n";
644 }
645 }
646
647 // Save
648 if ($object->status == $object::STATUS_VALIDATED) {
649 if ($permissiontoupdatestock) {
650 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";
651 } else {
652 print '<a class="butActionRefused classfortooltip" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans('MakeMovementsAndClose').'</a>'."\n";
653 }
654
655 if ($permissiontoupdatestock) {
656 print '<a class="butActionDelete" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=confirm_cancel&page='.$page.$paramwithsearch.'&token='.newToken().'">'.$langs->trans("Cancel").'</a>'."\n";
657 }
658 }
659 }
660 print '</div>'."\n";
661
662 if ($object->status != Inventory::STATUS_DRAFT && $object->status != Inventory::STATUS_VALIDATED) {
663 print '<br><br>';
664 }
665}
666
667
668
669if ($object->status == Inventory::STATUS_VALIDATED) {
670 print '<center>';
671 if (!empty($conf->use_javascript_ajax)) {
672 if ($permissiontoupdatestock) {
673 // Link to launch scan tool
674 if (isModEnabled('barcode') || isModEnabled('productbatch')) {
675 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>';
676 }
677
678 // Link to autofill
679 print '<a id="fillwithexpected" class="marginrightonly paddingright marginleftonly paddingleft" href="#">'.img_picto('', 'autofill', 'class="paddingrightonly"').$langs->trans('AutofillWithExpected').'</a>';
680 print '<script>';
681 print '$( document ).ready(function() {';
682 print ' $("#fillwithexpected").on("click",function fillWithExpected(){
683 $(".expectedqty").each(function(){
684 var object = $(this)[0];
685 var objecttofill = $("#"+object.id+"_input")[0];
686 objecttofill.value = object.innerText;
687 jQuery(".realqty").trigger("change");
688 })
689 console.log("Values filled (after click on fillwithexpected)");
690 /* disablebuttonmakemovementandclose(); */
691 return false;
692 });';
693 print '});';
694 print '</script>';
695
696 // Link to reset qty
697 print '<a href="#" id="clearqty" class="marginrightonly paddingright marginleftonly paddingleft">'.img_picto('', 'eraser', 'class="paddingrightonly"').$langs->trans("ClearQtys").'</a>';
698 } else {
699 print '<a class="classfortooltip marginrightonly paddingright marginleftonly paddingleft" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans("Save").'</a>'."\n";
700 }
701 }
702 print '<br>';
703 print '<br>';
704 print '</center>';
705}
706
707
708// Popup for mass barcode scanning
709if ($action == 'updatebyscaning') {
710 if ($permissiontoupdatestock) {
711 // Output the javascript to manage the scanner tool.
712 print '<script>';
713
714 print '
715 var duplicatedbatchcode = [];
716 var errortab1 = [];
717 var errortab2 = [];
718 var errortab3 = [];
719 var errortab4 = [];
720
721 function barcodescannerjs(){
722 console.log("We catch inputs in scanner box");
723 jQuery("#scantoolmessage").text();
724
725 var selectaddorreplace = $("select[name=selectaddorreplace]").val();
726 var barcodemode = $("input[name=barcodemode]:checked").val();
727 var barcodeproductqty = $("input[name=barcodeproductqty]").val();
728 var textarea = $("textarea[name=barcodelist]").val();
729 var textarray = textarea.split(/[\s,;]+/);
730 var tabproduct = [];
731 duplicatedbatchcode = [];
732 errortab1 = [];
733 errortab2 = [];
734 errortab3 = [];
735 errortab4 = [];
736
737 textarray = textarray.filter(function(value){
738 return value != "";
739 });
740 if(textarray.some((element) => element != "")){
741 $(".expectedqty").each(function(){
742 id = this.id;
743 console.log("Analyze the line "+id+" in inventory, barcodemode="+barcodemode);
744 warehouse = $("#"+id+"_warehouse").attr(\'data-ref\');
745 //console.log(warehouse);
746 productbarcode = $("#"+id+"_product").attr(\'data-barcode\');
747 //console.log(productbarcode);
748 productbatchcode = $("#"+id+"_batch").attr(\'data-batch\');
749 //console.log(productbatchcode);
750
751 if (barcodemode != "barcodeforproduct") {
752 tabproduct.forEach(product=>{
753 console.log("product.Batch="+product.Batch+" productbatchcode="+productbatchcode);
754 if(product.Batch != "" && product.Batch == productbatchcode){
755 console.log("duplicate batch code found for batch code "+productbatchcode);
756 duplicatedbatchcode.push(productbatchcode);
757 }
758 })
759 }
760 productinput = $("#"+id+"_input").val();
761 if(productinput == ""){
762 productinput = 0
763 }
764 tabproduct.push({\'Id\':id,\'Warehouse\':warehouse,\'Barcode\':productbarcode,\'Batch\':productbatchcode,\'Qty\':productinput,\'fetched\':false});
765 });
766
767 console.log("Loop on each record entered in the textarea");
768 textarray.forEach(function(element,index){
769 console.log("Process record element="+element+" id="+id);
770 var verify_batch = false;
771 var verify_barcode = false;
772 switch(barcodemode){
773 case "barcodeforautodetect":
774 verify_barcode = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"barcode",true);
775 verify_batch = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"lotserial",true);
776 break;
777 case "barcodeforproduct":
778 verify_barcode = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"barcode");
779 break;
780 case "barcodeforlotserial":
781 verify_batch = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"lotserial");
782 break;
783 default:
784 alert(\''.dol_escape_js($langs->trans("ErrorWrongBarcodemode")).' "\'+barcodemode+\'"\');
785 throw \''.dol_escape_js($langs->trans('ErrorWrongBarcodemode')).' "\'+barcodemode+\'"\';
786 }
787
788 if (verify_batch == false && verify_barcode == false) { /* If the 2 flags are false, not found error */
789 errortab2.push(element);
790 } else if (verify_batch == true && verify_barcode == true) { /* If the 2 flags are true, error: we don t know which one to take */
791 errortab3.push(element);
792 } else if (verify_batch == true) {
793 console.log("element="+element);
794 console.log(duplicatedbatchcode);
795 if (duplicatedbatchcode.includes(element)) {
796 errortab1.push(element);
797 }
798 }
799 });
800
801 if (Object.keys(errortab1).length < 1 && Object.keys(errortab2).length < 1 && Object.keys(errortab3).length < 1) {
802 tabproduct.forEach(product => {
803 if(product.Qty!=0){
804 console.log("We change #"+product.Id+"_input to match input in scanner box");
805 if(product.hasOwnProperty("reelqty")){
806 $.ajax({ url: \''.DOL_URL_ROOT.'/product/inventory/ajax/searchfrombarcode.php\',
807 data: { "token":"'.currentToken().'", "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},
808 type: \'POST\',
809 async: false,
810 success: function(response) {
811 if (typeof response == "object") {
812 console.log("response is already type object, no need to parse it");
813 } else {
814 console.log("response is type "+(typeof response));
815 response = JSON.parse(response);
816 }
817 if(response.status == "success"){
818 console.log(response.message);
819 $("<input type=\'text\' value=\'"+product.Qty+"\' />")
820 .attr("id", "id_"+response.id_line+"_input")
821 .attr("name", "id_"+response.id_line)
822 .appendTo("#formrecord");
823 }else{
824 console.error(response.message);
825 }
826 },
827 error : function(output) {
828 console.error("Error on line creation function");
829 },
830 });
831 } else {
832 $("#"+product.Id+"_input").val(product.Qty);
833 }
834 }
835 });
836 jQuery("#scantoolmessage").text("'.dol_escape_js($langs->transnoentities("QtyWasAddedToTheScannedBarcode")).'\n");
837 /* document.forms["formrecord"].submit(); */
838 } else {
839 let stringerror = "";
840 if (Object.keys(errortab1).length > 0) {
841 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorSameBatchNumber')).': ";
842 errortab1.forEach(element => {
843 stringerror += (element + ", ")
844 });
845 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
846 }
847 if (Object.keys(errortab2).length > 0) {
848 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorCantFindCodeInInventory')).': ";
849 errortab2.forEach(element => {
850 stringerror += (element + ", ")
851 });
852 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
853 }
854 if (Object.keys(errortab3).length > 0) {
855 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorCodeScannedIsBothProductAndSerial')).': ";
856 errortab3.forEach(element => {
857 stringerror += (element + ", ")
858 });
859 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
860 }
861 if (Object.keys(errortab4).length > 0) {
862 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorBarcodeNotFoundForProductWarehouse')).': ";
863 errortab4.forEach(element => {
864 stringerror += (element + ", ")
865 });
866 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
867 }
868
869 jQuery("#scantoolmessage").html(\''.dol_escape_js($langs->transnoentities("ErrorOnElementsInventory")).'\' + stringerror);
870 //alert("'.dol_escape_js($langs->trans("ErrorOnElementsInventory")).' :\n" + stringerror);
871 }
872 }
873
874 }
875
876 /* This methode is called by parent barcodescannerjs() */
877 function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,mode,autodetect=false){
878 BarcodeIsInProduct=0;
879 newproductrow=0
880 result=false;
881 tabproduct.forEach(product => {
882 $.ajax({ url: \''.DOL_URL_ROOT.'/product/inventory/ajax/searchfrombarcode.php\',
883 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},
884 type: \'POST\',
885 async: false,
886 success: function(response) {
887 if (typeof response == "object") {
888 console.log("response is already type object, no need to parse it");
889 } else {
890 console.log("response is type "+(typeof response));
891 response = JSON.parse(response);
892 }
893 if (response.status == "success"){
894 console.log(response.message);
895 if(!newproductrow){
896 newproductrow = response.object;
897 }
898 }else{
899 if (mode!="lotserial" && autodetect==false && !errortab4.includes(element)){
900 errortab4.push(element);
901 console.error(response.message);
902 }
903 }
904 },
905 error : function(output) {
906 console.error("Error on barcodeserialforproduct function");
907 },
908 });
909 console.log("Product "+(index+=1)+": "+element);
910 if(mode == "barcode"){
911 testonproduct = product.Barcode
912 }else if (mode == "lotserial"){
913 testonproduct = product.Batch
914 }
915 if(testonproduct == element){
916 if(selectaddorreplace == "add"){
917 productqty = parseInt(product.Qty,10);
918 product.Qty = productqty + parseInt(barcodeproductqty,10);
919 }else if(selectaddorreplace == "replace"){
920 if(product.fetched == false){
921 product.Qty = barcodeproductqty
922 product.fetched=true
923 }else{
924 productqty = parseInt(product.Qty,10);
925 product.Qty = productqty + parseInt(barcodeproductqty,10);
926 }
927 }
928 BarcodeIsInProduct+=1;
929 }
930 })
931 if(BarcodeIsInProduct==0 && newproductrow!=0){
932 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});
933 result = true;
934 }
935 if(BarcodeIsInProduct > 0){
936 result = true;
937 }
938 return result;
939 }
940 ';
941 print '</script>';
942 }
943 include DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
944 $formother = new FormOther($db);
945 print $formother->getHTMLScannerForm("barcodescannerjs", 'all');
946}
947
948//Call method to undo changes in real qty
949print '<script>';
950print 'jQuery(document).ready(function() {
951 $("#clearqty").on("click", function() {
952 console.log("Clear all values");
953 /* disablebuttonmakemovementandclose(); */
954 jQuery(".realqty").val("");
955 jQuery(".realqty").trigger("change");
956 return false; /* disable submit */
957 });
958 $(".undochangesqty").on("click", function undochangesqty() {
959 console.log("Clear value of inventory line");
960 id = this.id;
961 id = id.split("_")[1];
962 tmpvalue = $("#id_"+id+"_input_tmp").val()
963 $("#id_"+id+"_input")[0].value = tmpvalue;
964 /* disablebuttonmakemovementandclose(); */
965 return false; /* disable submit */
966 });
967});';
968print '</script>';
969
970print '<div class="fichecenter">';
971//print '<div class="fichehalfleft">';
972print '<div class="clearboth"></div>';
973
974//print load_fiche_titre($langs->trans('Consumption'), '', '');
975
976print '<div class="div-table-responsive-no-min">';
977print '<table id="tablelines" class="noborder noshadow centpercent">';
978
979print '<tr class="liste_titre">';
980print getTitleFieldOfList($langs->trans("Warehouse"), 0, $_SERVER['PHP_SELF'], 'e.ref', '', 'id=' . $object->id . '&page=' . $page . $param, '', $sortfield, $sortorder, '', 0, '') . "\n";
981print getTitleFieldOfList($langs->trans("Product"), 0, $_SERVER['PHP_SELF'], 'p.ref', '', 'id=' . $object->id . '&page=' . $page . $param, '', $sortfield, $sortorder, '', 0, '') . "\n";
982if (isModEnabled('productbatch')) {
983 print '<td>';
984 print $langs->trans("Batch");
985 print '</td>';
986}
987if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
988 // Expected quantity = If inventory is open: Quantity currently in stock (may change if stock movement are done during the inventory)
989 print '<td class="right">'.$form->textwithpicto($langs->trans("ExpectedQty"), $langs->trans("QtyCurrentlyKnownInStock")).'</td>';
990} else {
991 // Expected quantity = If inventory is closed: Quantity we had in stock when we start the inventory.
992 print '<td class="right">'.$form->textwithpicto($langs->trans("ExpectedQty"), $langs->trans("QtyInStockWhenInventoryWasValidated")).'</td>';
993}
994if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
995 print '<td class="right">'.$langs->trans('PMPExpected').'</td>';
996 print '<td class="right">'.$langs->trans('ExpectedValuation').'</td>';
997 print '<td class="right">'.$form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp")).'</td>';
998 print '<td class="right">'.$langs->trans('PMPReal').'</td>';
999 print '<td class="right">'.$langs->trans('RealValuation').'</td>';
1000} else {
1001 print '<td class="right">';
1002 print $form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp"));
1003 print '</td>';
1004}
1005if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1006 // Actions or link to stock movement
1007 print '<td class="center">';
1008 print '</td>';
1009} else {
1010 // Actions or link to stock movement
1011 print '<td class="right">';
1012 //print $langs->trans("StockMovement");
1013 print '</td>';
1014}
1015print '</tr>';
1016
1017// Line to add a new line in inventory
1018if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1019 print '<tr>';
1020 print '<td>';
1021 print $formproduct->selectWarehouses((GETPOSTISSET('fk_warehouse') ? GETPOSTINT('fk_warehouse') : $object->fk_warehouse), 'fk_warehouse', 'warehouseopen', 1, 0, 0, '', 0, 0, array(), 'maxwidth300');
1022 print '</td>';
1023 print '<td>';
1024 if (getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
1025 $filtertype = '';
1026 } else {
1027 $filtertype = 0;
1028 }
1029 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');
1030 print '</td>';
1031 if (isModEnabled('productbatch')) {
1032 print '<td>';
1033 print '<input type="text" name="batch" class="maxwidth100" value="'.(GETPOSTISSET('batch') ? GETPOST('batch') : '').'">';
1034 print '</td>';
1035 }
1036 print '<td class="right"></td>';
1037 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1038 print '<td class="right">';
1039 print '</td>';
1040 print '<td class="right">';
1041 print '</td>';
1042 print '<td class="right">';
1043 print '<input type="text" name="qtytoadd" class="maxwidth75" value="">';
1044 print '</td>';
1045 print '<td class="right">';
1046 print '</td>';
1047 print '<td class="right">';
1048 print '</td>';
1049 } else {
1050 print '<td class="right">';
1051 print '<input type="text" name="qtytoadd" class="maxwidth75" value="">';
1052 print '</td>';
1053 }
1054 // Actions
1055 print '<td class="center">';
1056 if ($permissiontoupdatestock) {
1057 print '<input type="submit" class="button paddingright" name="addline" value="'.$langs->trans("Add").'">';
1058 } else {
1059 print '<input type="submit" class="button paddingright" disabled="disabled" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'" name="addline" value="'.$langs->trans("Add").'">';
1060 }
1061 print '</td>';
1062 print '</tr>';
1063}
1064
1065// Request to show lines of inventory (prefilled after start/validate step)
1066$sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
1067$sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated, id.fk_movement, id.pmp_real, id.pmp_expected';
1068$sql .= ' FROM ' . $db->prefix() . 'inventorydet as id';
1069$sql .= ' LEFT JOIN ' . $db->prefix() . 'product as p ON id.fk_product = p.rowid';
1070$sql .= ' LEFT JOIN ' . $db->prefix() . 'entrepot as e ON id.fk_warehouse = e.rowid';
1071$sql .= ' WHERE id.fk_inventory = ' . ((int) $object->id);
1072$sql .= $db->order($sortfield, $sortorder);
1073$sql .= $db->plimit($limit, $offset);
1074
1075$cacheOfProducts = array();
1076$cacheOfWarehouses = array();
1077
1078//$sql = '';
1079$resql = $db->query($sql);
1080if ($resql) {
1081 $num = $db->num_rows($resql);
1082
1083 if (!empty($limit != 0) || $num > $limit || $page) {
1084 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);
1085 }
1086
1087 $i = 0;
1088 $hasinput = false;
1089 $totalarray = array();
1090 while ($i < $num) {
1091 $obj = $db->fetch_object($resql);
1092
1093 if (isset($cacheOfWarehouses[$obj->fk_warehouse])) {
1094 $warehouse_static = $cacheOfWarehouses[$obj->fk_warehouse];
1095 } else {
1096 $warehouse_static = new Entrepot($db);
1097 $warehouse_static->fetch($obj->fk_warehouse);
1098
1099 $cacheOfWarehouses[$warehouse_static->id] = $warehouse_static;
1100 }
1101
1102 // Load real stock we have now
1103 $option = '';
1104 if (isset($cacheOfProducts[$obj->fk_product])) {
1105 $product_static = $cacheOfProducts[$obj->fk_product];
1106 } else {
1107 $product_static = new Product($db);
1108 $result = $product_static->fetch($obj->fk_product, '', '', '', 1, 1, 1);
1109
1110 //$option = 'nobatch';
1111 $option .= ',novirtual';
1112 $product_static->load_stock($option); // Load stock_reel + stock_warehouse.
1113
1114 $cacheOfProducts[$product_static->id] = $product_static;
1115 }
1116
1117 print '<tr class="oddeven">';
1118 print '<td id="id_'.$obj->rowid.'_warehouse" data-ref="'.dol_escape_htmltag($warehouse_static->ref).'">';
1119 print $warehouse_static->getNomUrl(1);
1120 print '</td>';
1121 print '<td id="id_'.$obj->rowid.'_product" data-ref="'.dol_escape_htmltag($product_static->ref).'" data-barcode="'.dol_escape_htmltag($product_static->barcode).'">';
1122 print $product_static->getNomUrl(1).' - '.$product_static->label;
1123 print '</td>';
1124
1125 if (isModEnabled('productbatch')) {
1126 print '<td id="id_'.$obj->rowid.'_batch" data-batch="'.dol_escape_htmltag($obj->batch).'">';
1127 $batch_static = new Productlot($db);
1128 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1129 $res = $batch_static->fetch(0, $product_static->id, $obj->batch);
1130 if ($res) {
1131 print $batch_static->getNomUrl(1);
1132 } else {
1133 print dol_escape_htmltag($obj->batch);
1134 }
1135 print '</td>';
1136 }
1137
1138 // Expected quantity = If inventory is open: Quantity currently in stock (may change if stock movement are done during the inventory)
1139 // Expected quantity = If inventory is closed: Quantity we had in stock when we start the inventory.
1140 print '<td class="right expectedqty" id="id_'.$obj->rowid.'" title="Stock viewed at last update: '.$obj->qty_stock.'">';
1141 $valuetoshow = $obj->qty_stock;
1142 // For inventory not yet close, we overwrite with the real value in stock now
1143 if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1144 if (isModEnabled('productbatch') && $product_static->hasbatch()) {
1145 $valuetoshow = $product_static->stock_warehouse[$obj->fk_warehouse]->detail_batch[$obj->batch]->qty ?? 0;
1146 } else {
1147 $valuetoshow = !empty($product_static->stock_warehouse[$obj->fk_warehouse]->real) ? $product_static->stock_warehouse[$obj->fk_warehouse]->real : 0;
1148 }
1149 }
1150 print price2num($valuetoshow, 'MS');
1151 print '<input type="hidden" name="stock_qty_'.$obj->rowid.'" value="'.$valuetoshow.'">';
1152 print '</td>';
1153
1154 // Real quantity
1155 if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1156 $qty_view = GETPOST("id_".$obj->rowid) && price2num(GETPOST("id_".$obj->rowid), 'MS') >= 0 ? GETPOST("id_".$obj->rowid) : $obj->qty_view;
1157
1158 //if (!$hasinput && $qty_view !== null && $obj->qty_stock != $qty_view) {
1159 if ($qty_view != '') {
1160 $hasinput = true;
1161 }
1162
1163 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1164 //PMP Expected
1165 if (!empty($obj->pmp_expected)) {
1166 $pmp_expected = $obj->pmp_expected;
1167 } else {
1168 $pmp_expected = $product_static->pmp;
1169 }
1170 $pmp_valuation = $pmp_expected * $valuetoshow;
1171 print '<td class="right">';
1172 print is_null($pmp_expected) ? '' : price($pmp_expected);
1173 print '<input type="hidden" name="expectedpmp_'.$obj->rowid.'" value="'.$pmp_expected.'"/>';
1174 print '</td>';
1175 print '<td class="right">';
1176 print price($pmp_valuation);
1177 print '</td>';
1178
1179 print '<td class="right">';
1180 print '<a id="undochangesqty_'.$obj->rowid.'" href="#" class="undochangesqty reposition marginrightonly" title="'.dol_escape_htmltag($langs->trans("Clear")).'">';
1181 print img_picto('', 'eraser', 'class="opacitymedium"');
1182 print '</a>';
1183 print '<input type="text" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input" value="'.$qty_view.'">';
1184 print '</td>';
1185
1186 //PMP Real
1187 print '<td class="right">';
1188 if (!empty($obj->pmp_real) || (string) $obj->pmp_real === '0') {
1189 $pmp_real = $obj->pmp_real;
1190 } else {
1191 $pmp_real = $product_static->pmp;
1192 }
1193 $pmp_valuation_real = $pmp_real * $qty_view;
1194 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)).'">';
1195 print '</td>';
1196 print '<td class="right">';
1197 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.'">';
1198 print '</td>';
1199
1200 $totalExpectedValuation += $pmp_valuation;
1201 $totalRealValuation += $pmp_valuation_real;
1202 } else {
1203 print '<td class="right">';
1204 print '<a id="undochangesqty_'.$obj->rowid.'" href="#" class="undochangesqty reposition marginrightonly" title="'.dol_escape_htmltag($langs->trans("Clear")).'">';
1205 print img_picto('', 'eraser', 'class="opacitymedium"');
1206 print '</a>';
1207 print '<input type="text" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input" value="'.$qty_view.'">';
1208 print '</td>';
1209 }
1210
1211 // Picto delete line
1212 print '<td class="right">';
1213 if ($permissiontoupdatestock) {
1214 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>';
1215 }
1216 $qty_tmp = price2num(GETPOST("id_".$obj->rowid."_input_tmp"), 'MS') >= 0 ? GETPOST("id_".$obj->rowid."_input_tmp") : $qty_view;
1217 print '<input type="hidden" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'_input_tmp" id="id_'.$obj->rowid.'_input_tmp" value="'.$qty_tmp.'">';
1218 print '</td>';
1219 } else {
1220 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1221 // PMP Expected
1222 if (!empty($obj->pmp_expected)) {
1223 $pmp_expected = $obj->pmp_expected;
1224 } else {
1225 $pmp_expected = $product_static->pmp;
1226 }
1227 $pmp_valuation = $pmp_expected * $valuetoshow;
1228 print '<td class="right">';
1229 print is_null($pmp_expected) ? '' : price($pmp_expected);
1230 print '</td>';
1231 print '<td class="right">';
1232 print price($pmp_valuation);
1233 print '</td>';
1234
1235 print '<td class="right nowraponall">';
1236 print $obj->qty_view; // qty found
1237 print '</td>';
1238
1239 // PMP Real
1240 print '<td class="right">';
1241 if (!empty($obj->pmp_real)) {
1242 $pmp_real = $obj->pmp_real;
1243 } else {
1244 $pmp_real = $product_static->pmp;
1245 }
1246 $pmp_valuation_real = $pmp_real * $obj->qty_view;
1247 print is_null($pmp_real) ? '' : price($pmp_real);
1248 print '</td>';
1249 print '<td class="right">';
1250 print price($pmp_valuation_real);
1251 print '</td>';
1252 print '<td class="nowraponall right">';
1253
1254 $totalExpectedValuation += $pmp_valuation;
1255 $totalRealValuation += $pmp_valuation_real;
1256 } else {
1257 print '<td class="right nowraponall">';
1258 print $obj->qty_view; // qty found
1259 print '</td>';
1260 }
1261 print '<td>';
1262 if ($obj->fk_movement > 0) {
1263 $stockmovment = new MouvementStock($db);
1264 $stockmovment->fetch($obj->fk_movement);
1265 print $stockmovment->getNomUrl(1, 'movements');
1266 }
1267 print '</td>';
1268 }
1269 print '</tr>';
1270
1271 $i++;
1272 }
1273} else {
1275}
1276if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1277 print '<tr class="liste_total">';
1278 print '<td colspan="4">'.$langs->trans("Total").'</td>';
1279 print '<td class="right" colspan="2">'.price($totalExpectedValuation).'</td>';
1280 print '<td class="right" id="totalRealValuation" colspan="3">'.price($totalRealValuation).'</td>';
1281 print '<td></td>';
1282 print '</tr>';
1283}
1284print '</table>';
1285
1286print '</div>';
1287
1288if ($object->status == $object::STATUS_VALIDATED) {
1289 print '<center><input id="submitrecord" type="submit" class="button button-save" name="save" value="'.$langs->trans("Save").'"></center>';
1290}
1291
1292print '</div>';
1293
1294
1295// Call method to disable the button if no qty entered yet for inventory
1296/*
1297if ($object->status != $object::STATUS_VALIDATED || !$hasinput) {
1298 print '<script type="text/javascript">
1299 jQuery(document).ready(function() {
1300 console.log("Call disablebuttonmakemovementandclose because status = '.((int) $object->status).' or $hasinput = '.((int) $hasinput).'");
1301 disablebuttonmakemovementandclose();
1302 });
1303 </script>';
1304}
1305*/
1306
1307print '</form>';
1308
1309print '<script type="text/javascript">
1310 $(document).ready(function() {
1311
1312 $(".paginationnext:last").click(function(e){
1313 var form = $("#formrecord");
1314 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1315 $.ajax({
1316 url: actionURL,
1317 data: form.serialize(),
1318 cache: false,
1319 success: function(result){
1320 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page + 1).$paramwithsearch.'";
1321 }});
1322 return false;
1323 });
1324
1325
1326 $(".paginationprevious:last").click(function(e){
1327 var form = $("#formrecord");
1328 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1329 $.ajax({
1330 url: actionURL,
1331 data: form.serialize(),
1332 cache: false,
1333 success: function(result){
1334 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page - 1).$paramwithsearch.'";
1335 }});
1336 return false;
1337 });
1338
1339 $("#idbuttonmakemovementandclose").click(function(e){
1340 var form = $("#formrecord");
1341 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1342 $.ajax({
1343 url: actionURL,
1344 type: "POST",
1345 data: form.serialize(),
1346 cache: false,
1347 success: function(result){
1348 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'&action=record";
1349 }});
1350 return false;
1351 });
1352 });
1353</script>';
1354
1355
1356if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1357 ?>
1358<script type="text/javascript">
1359$('.realqty').on('change', function () {
1360 let realqty = $(this).closest('tr').find('.realqty').val();
1361 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1362 let realpmp = $(inputPmp).val();
1363 if (!isNaN(realqty) && !isNaN(realpmp)) {
1364 let realval = realqty * realpmp;
1365 $(this).closest('tr').find('input[name^=realvaluation]').val(realval.toFixed(2));
1366 }
1367 updateTotalValuation();
1368});
1369
1370$('input[class*=realpmp]').on('change', function () {
1371 let inputQtyReal = $(this).closest('tr').find('.realqty');
1372 let realqty = $(inputQtyReal).val();
1373 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1374 console.log(inputPmp);
1375 let realPmpClassname = $(inputPmp).attr('class').match(/[\w-]*realpmp[\w-]*/g)[0];
1376 let realpmp = $(inputPmp).val();
1377 if (!isNaN(realpmp)) {
1378 $('.'+realPmpClassname).val(realpmp); //For batch case if pmp is changed we change it everywhere it's same product and calc back everything
1379
1380 if (!isNaN(realqty)) {
1381 let realval = realqty * realpmp;
1382 $(this).closest('tr').find('input[name^=realvaluation]').val(realval.toFixed(2));
1383 }
1384 $('.realqty').trigger('change');
1385 updateTotalValuation();
1386 }
1387});
1388
1389$('input[name^=realvaluation]').on('change', function () {
1390 let inputQtyReal = $(this).closest('tr').find('.realqty');
1391 let realqty = $(inputQtyReal).val();
1392 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1393 let inputRealValuation = $(this).closest('tr').find('input[name^=realvaluation]');
1394 let realPmpClassname = $(inputPmp).attr('class').match(/[\w-]*realpmp[\w-]*/g)[0];
1395 let realvaluation = $(inputRealValuation).val();
1396 if (!isNaN(realvaluation) && !isNaN(realqty) && realvaluation !== '' && realqty !== '' && realqty !== 0) {
1397 let realpmp = realvaluation / realqty
1398 $('.'+realPmpClassname).val(realpmp); //For batch case if pmp is changed we change it everywhere it's same product and calc back everything
1399 $('.realqty').trigger('change');
1400 updateTotalValuation();
1401 }
1402});
1403
1404function updateTotalValuation() {
1405 let total = 0;
1406 $('input[name^=realvaluation]').each(function( index ) {
1407 let val = $(this).val();
1408 if(!isNaN(val)) total += parseFloat($(this).val());
1409 });
1410 let currencyFractionDigits = new Intl.NumberFormat('fr-FR', {
1411 style: 'currency',
1412 currency: 'EUR',
1413 }).resolvedOptions().maximumFractionDigits;
1414 $('#totalRealValuation').html(total.toLocaleString('fr-FR', {
1415 maximumFractionDigits: currencyFractionDigits
1416 }));
1417}
1418
1419
1420</script>
1421 <?php
1422}
1423
1424// End of page
1425llxFooter();
1426$db->close();
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
$totalarray
Definition list.php:501
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 to help generate other html components 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.
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.
dol_now($mode='gmt')
Return date for now.
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.
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.
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'.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0, $nodefault=0)
Return value of a param into GET or POST supervariable.
getTitleFieldOfList($name, $thead=0, $file="", $field="", $begin="", $moreparam="", $moreattrib="", $sortfield="", $sortorder="", $prefix="", $disablesortlink=0, $tooltip='', $forcenowrapcolumntitle=0)
Get title line of an array.
GETPOSTINT($paramname, $method=0, $nodefault=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
GETPOSTISSET($paramname)
Return true if we are in a context of submitting the parameter $paramname from a POST of a form.
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.
isModEnabled($module)
Is Dolibarr module enabled.
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.
if(preg_match('/(crypted|dolcrypt):/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
'integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter[:Sortfield]]]',...
Definition repair.php:130
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.