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