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 response = JSON.parse(response);
807 if(response.status == "success"){
808 console.log(response.message);
809 $("<input type=\'text\' value=\'"+product.Qty+"\' />")
810 .attr("id", "id_"+response.id_line+"_input")
811 .attr("name", "id_"+response.id_line)
812 .appendTo("#formrecord");
813 }else{
814 console.error(response.message);
815 }
816 },
817 error : function(output) {
818 console.error("Error on line creation function");
819 },
820 });
821 } else {
822 $("#"+product.Id+"_input").val(product.Qty);
823 }
824 }
825 });
826 jQuery("#scantoolmessage").text("'.dol_escape_js($langs->transnoentities("QtyWasAddedToTheScannedBarcode")).'\n");
827 /* document.forms["formrecord"].submit(); */
828 } else {
829 let stringerror = "";
830 if (Object.keys(errortab1).length > 0) {
831 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorSameBatchNumber')).': ";
832 errortab1.forEach(element => {
833 stringerror += (element + ", ")
834 });
835 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
836 }
837 if (Object.keys(errortab2).length > 0) {
838 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorCantFindCodeInInventory')).': ";
839 errortab2.forEach(element => {
840 stringerror += (element + ", ")
841 });
842 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
843 }
844 if (Object.keys(errortab3).length > 0) {
845 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorCodeScannedIsBothProductAndSerial')).': ";
846 errortab3.forEach(element => {
847 stringerror += (element + ", ")
848 });
849 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
850 }
851 if (Object.keys(errortab4).length > 0) {
852 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorBarcodeNotFoundForProductWarehouse')).': ";
853 errortab4.forEach(element => {
854 stringerror += (element + ", ")
855 });
856 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
857 }
858
859 jQuery("#scantoolmessage").html(\''.dol_escape_js($langs->transnoentities("ErrorOnElementsInventory")).'\' + stringerror);
860 //alert("'.dol_escape_js($langs->trans("ErrorOnElementsInventory")).' :\n" + stringerror);
861 }
862 }
863
864 }
865
866 /* This methode is called by parent barcodescannerjs() */
867 function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,mode,autodetect=false){
868 BarcodeIsInProduct=0;
869 newproductrow=0
870 result=false;
871 tabproduct.forEach(product => {
872 $.ajax({ url: \''.DOL_URL_ROOT.'/product/inventory/ajax/searchfrombarcode.php\',
873 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},
874 type: \'POST\',
875 async: false,
876 success: function(response) {
877 response = JSON.parse(response);
878 if (response.status == "success"){
879 console.log(response.message);
880 if(!newproductrow){
881 newproductrow = response.object;
882 }
883 }else{
884 if (mode!="lotserial" && autodetect==false && !errortab4.includes(element)){
885 errortab4.push(element);
886 console.error(response.message);
887 }
888 }
889 },
890 error : function(output) {
891 console.error("Error on barcodeserialforproduct function");
892 },
893 });
894 console.log("Product "+(index+=1)+": "+element);
895 if(mode == "barcode"){
896 testonproduct = product.Barcode
897 }else if (mode == "lotserial"){
898 testonproduct = product.Batch
899 }
900 if(testonproduct == element){
901 if(selectaddorreplace == "add"){
902 productqty = parseInt(product.Qty,10);
903 product.Qty = productqty + parseInt(barcodeproductqty,10);
904 }else if(selectaddorreplace == "replace"){
905 if(product.fetched == false){
906 product.Qty = barcodeproductqty
907 product.fetched=true
908 }else{
909 productqty = parseInt(product.Qty,10);
910 product.Qty = productqty + parseInt(barcodeproductqty,10);
911 }
912 }
913 BarcodeIsInProduct+=1;
914 }
915 })
916 if(BarcodeIsInProduct==0 && newproductrow!=0){
917 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});
918 result = true;
919 }
920 if(BarcodeIsInProduct > 0){
921 result = true;
922 }
923 return result;
924 }
925 ';
926 print '</script>';
927 }
928 include DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
929 $formother = new FormOther($db);
930 print $formother->getHTMLScannerForm("barcodescannerjs", 'all');
931}
932
933//Call method to undo changes in real qty
934print '<script>';
935print 'jQuery(document).ready(function() {
936 $("#clearqty").on("click", function() {
937 console.log("Clear all values");
938 /* disablebuttonmakemovementandclose(); */
939 jQuery(".realqty").val("");
940 jQuery(".realqty").trigger("change");
941 return false; /* disable submit */
942 });
943 $(".undochangesqty").on("click", function undochangesqty() {
944 console.log("Clear value of inventory line");
945 id = this.id;
946 id = id.split("_")[1];
947 tmpvalue = $("#id_"+id+"_input_tmp").val()
948 $("#id_"+id+"_input")[0].value = tmpvalue;
949 /* disablebuttonmakemovementandclose(); */
950 return false; /* disable submit */
951 });
952});';
953print '</script>';
954
955print '<div class="fichecenter">';
956//print '<div class="fichehalfleft">';
957print '<div class="clearboth"></div>';
958
959//print load_fiche_titre($langs->trans('Consumption'), '', '');
960
961print '<div class="div-table-responsive-no-min">';
962print '<table id="tablelines" class="noborder noshadow centpercent">';
963
964print '<tr class="liste_titre">';
965print getTitleFieldOfList($langs->trans("Warehouse"), 0, $_SERVER['PHP_SELF'], 'e.ref', '', 'id=' . $object->id . '&page=' . $page . $param, '', $sortfield, $sortorder, '', 0, '') . "\n";
966print getTitleFieldOfList($langs->trans("Product"), 0, $_SERVER['PHP_SELF'], 'p.ref', '', 'id=' . $object->id . '&page=' . $page . $param, '', $sortfield, $sortorder, '', 0, '') . "\n";
967if (isModEnabled('productbatch')) {
968 print '<td>';
969 print $langs->trans("Batch");
970 print '</td>';
971}
972if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
973 // Expected quantity = If inventory is open: Quantity currently in stock (may change if stock movement are done during the inventory)
974 print '<td class="right">'.$form->textwithpicto($langs->trans("ExpectedQty"), $langs->trans("QtyCurrentlyKnownInStock")).'</td>';
975} else {
976 // Expected quantity = If inventory is closed: Quantity we had in stock when we start the inventory.
977 print '<td class="right">'.$form->textwithpicto($langs->trans("ExpectedQty"), $langs->trans("QtyInStockWhenInventoryWasValidated")).'</td>';
978}
979if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
980 print '<td class="right">'.$langs->trans('PMPExpected').'</td>';
981 print '<td class="right">'.$langs->trans('ExpectedValuation').'</td>';
982 print '<td class="right">'.$form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp")).'</td>';
983 print '<td class="right">'.$langs->trans('PMPReal').'</td>';
984 print '<td class="right">'.$langs->trans('RealValuation').'</td>';
985} else {
986 print '<td class="right">';
987 print $form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp"));
988 print '</td>';
989}
990if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
991 // Actions or link to stock movement
992 print '<td class="center">';
993 print '</td>';
994} else {
995 // Actions or link to stock movement
996 print '<td class="right">';
997 //print $langs->trans("StockMovement");
998 print '</td>';
999}
1000print '</tr>';
1001
1002// Line to add a new line in inventory
1003if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1004 print '<tr>';
1005 print '<td>';
1006 print $formproduct->selectWarehouses((GETPOSTISSET('fk_warehouse') ? GETPOSTINT('fk_warehouse') : $object->fk_warehouse), 'fk_warehouse', 'warehouseopen', 1, 0, 0, '', 0, 0, array(), 'maxwidth300');
1007 print '</td>';
1008 print '<td>';
1009 if (getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
1010 $filtertype = '';
1011 } else {
1012 $filtertype = 0;
1013 }
1014 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');
1015 print '</td>';
1016 if (isModEnabled('productbatch')) {
1017 print '<td>';
1018 print '<input type="text" name="batch" class="maxwidth100" value="'.(GETPOSTISSET('batch') ? GETPOST('batch') : '').'">';
1019 print '</td>';
1020 }
1021 print '<td class="right"></td>';
1022 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1023 print '<td class="right">';
1024 print '</td>';
1025 print '<td class="right">';
1026 print '</td>';
1027 print '<td class="right">';
1028 print '<input type="text" name="qtytoadd" class="maxwidth75" value="">';
1029 print '</td>';
1030 print '<td class="right">';
1031 print '</td>';
1032 print '<td class="right">';
1033 print '</td>';
1034 } else {
1035 print '<td class="right">';
1036 print '<input type="text" name="qtytoadd" class="maxwidth75" value="">';
1037 print '</td>';
1038 }
1039 // Actions
1040 print '<td class="center">';
1041 if ($permissiontoupdatestock) {
1042 print '<input type="submit" class="button paddingright" name="addline" value="'.$langs->trans("Add").'">';
1043 } else {
1044 print '<input type="submit" class="button paddingright" disabled="disabled" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'" name="addline" value="'.$langs->trans("Add").'">';
1045 }
1046 print '</td>';
1047 print '</tr>';
1048}
1049
1050// Request to show lines of inventory (prefilled after start/validate step)
1051$sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
1052$sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated, id.fk_movement, id.pmp_real, id.pmp_expected';
1053$sql .= ' FROM ' . $db->prefix() . 'inventorydet as id';
1054$sql .= ' LEFT JOIN ' . $db->prefix() . 'product as p ON id.fk_product = p.rowid';
1055$sql .= ' LEFT JOIN ' . $db->prefix() . 'entrepot as e ON id.fk_warehouse = e.rowid';
1056$sql .= ' WHERE id.fk_inventory = ' . ((int) $object->id);
1057$sql .= $db->order($sortfield, $sortorder);
1058$sql .= $db->plimit($limit, $offset);
1059
1060$cacheOfProducts = array();
1061$cacheOfWarehouses = array();
1062
1063//$sql = '';
1064$resql = $db->query($sql);
1065if ($resql) {
1066 $num = $db->num_rows($resql);
1067
1068 if (!empty($limit != 0) || $num > $limit || $page) {
1069 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);
1070 }
1071
1072 $i = 0;
1073 $hasinput = false;
1074 $totalarray = array();
1075 while ($i < $num) {
1076 $obj = $db->fetch_object($resql);
1077
1078 if (isset($cacheOfWarehouses[$obj->fk_warehouse])) {
1079 $warehouse_static = $cacheOfWarehouses[$obj->fk_warehouse];
1080 } else {
1081 $warehouse_static = new Entrepot($db);
1082 $warehouse_static->fetch($obj->fk_warehouse);
1083
1084 $cacheOfWarehouses[$warehouse_static->id] = $warehouse_static;
1085 }
1086
1087 // Load real stock we have now
1088 $option = '';
1089 if (isset($cacheOfProducts[$obj->fk_product])) {
1090 $product_static = $cacheOfProducts[$obj->fk_product];
1091 } else {
1092 $product_static = new Product($db);
1093 $result = $product_static->fetch($obj->fk_product, '', '', '', 1, 1, 1);
1094
1095 //$option = 'nobatch';
1096 $option .= ',novirtual';
1097 $product_static->load_stock($option); // Load stock_reel + stock_warehouse.
1098
1099 $cacheOfProducts[$product_static->id] = $product_static;
1100 }
1101
1102 print '<tr class="oddeven">';
1103 print '<td id="id_'.$obj->rowid.'_warehouse" data-ref="'.dol_escape_htmltag($warehouse_static->ref).'">';
1104 print $warehouse_static->getNomUrl(1);
1105 print '</td>';
1106 print '<td id="id_'.$obj->rowid.'_product" data-ref="'.dol_escape_htmltag($product_static->ref).'" data-barcode="'.dol_escape_htmltag($product_static->barcode).'">';
1107 print $product_static->getNomUrl(1).' - '.$product_static->label;
1108 print '</td>';
1109
1110 if (isModEnabled('productbatch')) {
1111 print '<td id="id_'.$obj->rowid.'_batch" data-batch="'.dol_escape_htmltag($obj->batch).'">';
1112 $batch_static = new Productlot($db);
1113 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1114 $res = $batch_static->fetch(0, $product_static->id, $obj->batch);
1115 if ($res) {
1116 print $batch_static->getNomUrl(1);
1117 } else {
1118 print dol_escape_htmltag($obj->batch);
1119 }
1120 print '</td>';
1121 }
1122
1123 // Expected quantity = If inventory is open: Quantity currently in stock (may change if stock movement are done during the inventory)
1124 // Expected quantity = If inventory is closed: Quantity we had in stock when we start the inventory.
1125 print '<td class="right expectedqty" id="id_'.$obj->rowid.'" title="Stock viewed at last update: '.$obj->qty_stock.'">';
1126 $valuetoshow = $obj->qty_stock;
1127 // For inventory not yet close, we overwrite with the real value in stock now
1128 if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1129 if (isModEnabled('productbatch') && $product_static->hasbatch()) {
1130 $valuetoshow = $product_static->stock_warehouse[$obj->fk_warehouse]->detail_batch[$obj->batch]->qty ?? 0;
1131 } else {
1132 $valuetoshow = !empty($product_static->stock_warehouse[$obj->fk_warehouse]->real) ? $product_static->stock_warehouse[$obj->fk_warehouse]->real : 0;
1133 }
1134 }
1135 print price2num($valuetoshow, 'MS');
1136 print '<input type="hidden" name="stock_qty_'.$obj->rowid.'" value="'.$valuetoshow.'">';
1137 print '</td>';
1138
1139 // Real quantity
1140 if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1141 $qty_view = GETPOST("id_".$obj->rowid) && price2num(GETPOST("id_".$obj->rowid), 'MS') >= 0 ? GETPOST("id_".$obj->rowid) : $obj->qty_view;
1142
1143 //if (!$hasinput && $qty_view !== null && $obj->qty_stock != $qty_view) {
1144 if ($qty_view != '') {
1145 $hasinput = true;
1146 }
1147
1148 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1149 //PMP Expected
1150 if (!empty($obj->pmp_expected)) {
1151 $pmp_expected = $obj->pmp_expected;
1152 } else {
1153 $pmp_expected = $product_static->pmp;
1154 }
1155 $pmp_valuation = $pmp_expected * $valuetoshow;
1156 print '<td class="right">';
1157 print is_null($pmp_expected) ? '' : price($pmp_expected);
1158 print '<input type="hidden" name="expectedpmp_'.$obj->rowid.'" value="'.$pmp_expected.'"/>';
1159 print '</td>';
1160 print '<td class="right">';
1161 print price($pmp_valuation);
1162 print '</td>';
1163
1164 print '<td class="right">';
1165 print '<a id="undochangesqty_'.$obj->rowid.'" href="#" class="undochangesqty reposition marginrightonly" title="'.dol_escape_htmltag($langs->trans("Clear")).'">';
1166 print img_picto('', 'eraser', 'class="opacitymedium"');
1167 print '</a>';
1168 print '<input type="text" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input" value="'.$qty_view.'">';
1169 print '</td>';
1170
1171 //PMP Real
1172 print '<td class="right">';
1173 if (!empty($obj->pmp_real) || (string) $obj->pmp_real === '0') {
1174 $pmp_real = $obj->pmp_real;
1175 } else {
1176 $pmp_real = $product_static->pmp;
1177 }
1178 $pmp_valuation_real = $pmp_real * $qty_view;
1179 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)).'">';
1180 print '</td>';
1181 print '<td class="right">';
1182 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.'">';
1183 print '</td>';
1184
1185 $totalExpectedValuation += $pmp_valuation;
1186 $totalRealValuation += $pmp_valuation_real;
1187 } else {
1188 print '<td class="right">';
1189 print '<a id="undochangesqty_'.$obj->rowid.'" href="#" class="undochangesqty reposition marginrightonly" title="'.dol_escape_htmltag($langs->trans("Clear")).'">';
1190 print img_picto('', 'eraser', 'class="opacitymedium"');
1191 print '</a>';
1192 print '<input type="text" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input" value="'.$qty_view.'">';
1193 print '</td>';
1194 }
1195
1196 // Picto delete line
1197 print '<td class="right">';
1198 if ($permissiontoupdatestock) {
1199 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>';
1200 }
1201 $qty_tmp = price2num(GETPOST("id_".$obj->rowid."_input_tmp"), 'MS') >= 0 ? GETPOST("id_".$obj->rowid."_input_tmp") : $qty_view;
1202 print '<input type="hidden" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'_input_tmp" id="id_'.$obj->rowid.'_input_tmp" value="'.$qty_tmp.'">';
1203 print '</td>';
1204 } else {
1205 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1206 // PMP Expected
1207 if (!empty($obj->pmp_expected)) {
1208 $pmp_expected = $obj->pmp_expected;
1209 } else {
1210 $pmp_expected = $product_static->pmp;
1211 }
1212 $pmp_valuation = $pmp_expected * $valuetoshow;
1213 print '<td class="right">';
1214 print is_null($pmp_expected) ? '' : price($pmp_expected);
1215 print '</td>';
1216 print '<td class="right">';
1217 print price($pmp_valuation);
1218 print '</td>';
1219
1220 print '<td class="right nowraponall">';
1221 print $obj->qty_view; // qty found
1222 print '</td>';
1223
1224 // PMP Real
1225 print '<td class="right">';
1226 if (!empty($obj->pmp_real)) {
1227 $pmp_real = $obj->pmp_real;
1228 } else {
1229 $pmp_real = $product_static->pmp;
1230 }
1231 $pmp_valuation_real = $pmp_real * $obj->qty_view;
1232 print is_null($pmp_real) ? '' : price($pmp_real);
1233 print '</td>';
1234 print '<td class="right">';
1235 print price($pmp_valuation_real);
1236 print '</td>';
1237 print '<td class="nowraponall right">';
1238
1239 $totalExpectedValuation += $pmp_valuation;
1240 $totalRealValuation += $pmp_valuation_real;
1241 } else {
1242 print '<td class="right nowraponall">';
1243 print $obj->qty_view; // qty found
1244 print '</td>';
1245 }
1246 print '<td>';
1247 if ($obj->fk_movement > 0) {
1248 $stockmovment = new MouvementStock($db);
1249 $stockmovment->fetch($obj->fk_movement);
1250 print $stockmovment->getNomUrl(1, 'movements');
1251 }
1252 print '</td>';
1253 }
1254 print '</tr>';
1255
1256 $i++;
1257 }
1258} else {
1259 dol_print_error($db);
1260}
1261if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1262 print '<tr class="liste_total">';
1263 print '<td colspan="4">'.$langs->trans("Total").'</td>';
1264 print '<td class="right" colspan="2">'.price($totalExpectedValuation).'</td>';
1265 print '<td class="right" id="totalRealValuation" colspan="3">'.price($totalRealValuation).'</td>';
1266 print '<td></td>';
1267 print '</tr>';
1268}
1269print '</table>';
1270
1271print '</div>';
1272
1273if ($object->status == $object::STATUS_VALIDATED) {
1274 print '<center><input id="submitrecord" type="submit" class="button button-save" name="save" value="'.$langs->trans("Save").'"></center>';
1275}
1276
1277print '</div>';
1278
1279
1280// Call method to disable the button if no qty entered yet for inventory
1281/*
1282if ($object->status != $object::STATUS_VALIDATED || !$hasinput) {
1283 print '<script type="text/javascript">
1284 jQuery(document).ready(function() {
1285 console.log("Call disablebuttonmakemovementandclose because status = '.((int) $object->status).' or $hasinput = '.((int) $hasinput).'");
1286 disablebuttonmakemovementandclose();
1287 });
1288 </script>';
1289}
1290*/
1291
1292print '</form>';
1293
1294print '<script type="text/javascript">
1295 $(document).ready(function() {
1296
1297 $(".paginationnext:last").click(function(e){
1298 var form = $("#formrecord");
1299 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1300 $.ajax({
1301 url: actionURL,
1302 data: form.serialize(),
1303 cache: false,
1304 success: function(result){
1305 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page + 1).$paramwithsearch.'";
1306 }});
1307 return false;
1308 });
1309
1310
1311 $(".paginationprevious:last").click(function(e){
1312 var form = $("#formrecord");
1313 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1314 $.ajax({
1315 url: actionURL,
1316 data: form.serialize(),
1317 cache: false,
1318 success: function(result){
1319 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page - 1).$paramwithsearch.'";
1320 }});
1321 return false;
1322 });
1323
1324 $("#idbuttonmakemovementandclose").click(function(e){
1325 var form = $("#formrecord");
1326 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1327 $.ajax({
1328 url: actionURL,
1329 type: "POST",
1330 data: form.serialize(),
1331 cache: false,
1332 success: function(result){
1333 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'&action=record";
1334 }});
1335 return false;
1336 });
1337 });
1338</script>';
1339
1340
1341if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1342 ?>
1343<script type="text/javascript">
1344$('.realqty').on('change', function () {
1345 let realqty = $(this).closest('tr').find('.realqty').val();
1346 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1347 let realpmp = $(inputPmp).val();
1348 if (!isNaN(realqty) && !isNaN(realpmp)) {
1349 let realval = realqty * realpmp;
1350 $(this).closest('tr').find('input[name^=realvaluation]').val(realval.toFixed(2));
1351 }
1352 updateTotalValuation();
1353});
1354
1355$('input[class*=realpmp]').on('change', function () {
1356 let inputQtyReal = $(this).closest('tr').find('.realqty');
1357 let realqty = $(inputQtyReal).val();
1358 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1359 console.log(inputPmp);
1360 let realPmpClassname = $(inputPmp).attr('class').match(/[\w-]*realpmp[\w-]*/g)[0];
1361 let realpmp = $(inputPmp).val();
1362 if (!isNaN(realpmp)) {
1363 $('.'+realPmpClassname).val(realpmp); //For batch case if pmp is changed we change it everywhere it's same product and calc back everything
1364
1365 if (!isNaN(realqty)) {
1366 let realval = realqty * realpmp;
1367 $(this).closest('tr').find('input[name^=realvaluation]').val(realval.toFixed(2));
1368 }
1369 $('.realqty').trigger('change');
1370 updateTotalValuation();
1371 }
1372});
1373
1374$('input[name^=realvaluation]').on('change', function () {
1375 let inputQtyReal = $(this).closest('tr').find('.realqty');
1376 let realqty = $(inputQtyReal).val();
1377 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1378 let inputRealValuation = $(this).closest('tr').find('input[name^=realvaluation]');
1379 let realPmpClassname = $(inputPmp).attr('class').match(/[\w-]*realpmp[\w-]*/g)[0];
1380 let realvaluation = $(inputRealValuation).val();
1381 if (!isNaN(realvaluation) && !isNaN(realqty) && realvaluation !== '' && realqty !== '' && realqty !== 0) {
1382 let realpmp = realvaluation / realqty
1383 $('.'+realPmpClassname).val(realpmp); //For batch case if pmp is changed we change it everywhere it's same product and calc back everything
1384 $('.realqty').trigger('change');
1385 updateTotalValuation();
1386 }
1387});
1388
1389function updateTotalValuation() {
1390 let total = 0;
1391 $('input[name^=realvaluation]').each(function( index ) {
1392 let val = $(this).val();
1393 if(!isNaN(val)) total += parseFloat($(this).val());
1394 });
1395 let currencyFractionDigits = new Intl.NumberFormat('fr-FR', {
1396 style: 'currency',
1397 currency: 'EUR',
1398 }).resolvedOptions().maximumFractionDigits;
1399 $('#totalRealValuation').html(total.toLocaleString('fr-FR', {
1400 maximumFractionDigits: currencyFractionDigits
1401 }));
1402}
1403
1404
1405</script>
1406 <?php
1407}
1408
1409// End of page
1410llxFooter();
1411$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.