dolibarr 24.0.0-beta
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 . '">';
617if ($backtopage) {
618 print '<input type="hidden" name="backtopage" value="'.$backtopage.'">';
619}
620
621
622// Buttons for actions
623if ($action != 'record') {
624 print '<div class="tabsAction">'."\n";
625 $parameters = array();
626 $reshook = $hookmanager->executeHooks('addMoreActionsButtons', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
627 if ($reshook < 0) {
628 setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
629 }
630
631 if (empty($reshook)) {
632 if ($object->status == Inventory::STATUS_DRAFT) {
633 if ($permissiontoupdatestock) {
634 if (getDolGlobalInt('INVENTORY_INCLUDE_SUB_WAREHOUSE') && !empty($object->fk_warehouse)) {
635 print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=validate&token='.newToken().'">'.$langs->trans("Validate").' ('.$langs->trans("Start").')</a>';
636 } else {
637 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>';
638 }
639 } else {
640 print '<a class="butActionRefused classfortooltip" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans('Validate').' ('.$langs->trans("Start").')</a>'."\n";
641 }
642 }
643
644 // Save
645 if ($object->status == $object::STATUS_VALIDATED) {
646 if ($permissiontoupdatestock) {
647 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";
648 } else {
649 print '<a class="butActionRefused classfortooltip" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans('MakeMovementsAndClose').'</a>'."\n";
650 }
651
652 if ($permissiontoupdatestock) {
653 print '<a class="butActionDelete" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=confirm_cancel&page='.$page.$paramwithsearch.'&token='.newToken().'">'.$langs->trans("Cancel").'</a>'."\n";
654 }
655 }
656 }
657 print '</div>'."\n";
658
659 if ($object->status != Inventory::STATUS_DRAFT && $object->status != Inventory::STATUS_VALIDATED) {
660 print '<br><br>';
661 }
662}
663
664
665
666if ($object->status == Inventory::STATUS_VALIDATED) {
667 print '<center>';
668 if (!empty($conf->use_javascript_ajax)) {
669 if ($permissiontoupdatestock) {
670 // Link to launch scan tool
671 if (isModEnabled('barcode') || isModEnabled('productbatch')) {
672 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>';
673 }
674
675 // Link to autofill
676 print '<a id="fillwithexpected" class="marginrightonly paddingright marginleftonly paddingleft" href="#">'.img_picto('', 'autofill', 'class="paddingrightonly"').$langs->trans('AutofillWithExpected').'</a>';
677 print '<script>';
678 print '$( document ).ready(function() {';
679 print ' $("#fillwithexpected").on("click",function fillWithExpected(){
680 $(".expectedqty").each(function(){
681 var object = $(this)[0];
682 var objecttofill = $("#"+object.id+"_input")[0];
683 objecttofill.value = object.innerText;
684 jQuery(".realqty").trigger("change");
685 })
686 console.log("Values filled (after click on fillwithexpected)");
687 /* disablebuttonmakemovementandclose(); */
688 return false;
689 });';
690 print '});';
691 print '</script>';
692
693 // Link to reset qty
694 print '<a href="#" id="clearqty" class="marginrightonly paddingright marginleftonly paddingleft">'.img_picto('', 'eraser', 'class="paddingrightonly"').$langs->trans("ClearQtys").'</a>';
695 } else {
696 print '<a class="classfortooltip marginrightonly paddingright marginleftonly paddingleft" href="#" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'">'.$langs->trans("Save").'</a>'."\n";
697 }
698 }
699 print '<br>';
700 print '<br>';
701 print '</center>';
702}
703
704
705// Popup for mass barcode scanning
706if ($action == 'updatebyscaning') {
707 if ($permissiontoupdatestock) {
708 // Output the javascript to manage the scanner tool.
709 print '<script>';
710
711 print '
712 var duplicatedbatchcode = [];
713 var errortab1 = [];
714 var errortab2 = [];
715 var errortab3 = [];
716 var errortab4 = [];
717
718 function barcodescannerjs(){
719 console.log("We catch inputs in scanner box");
720 jQuery("#scantoolmessage").text();
721
722 var selectaddorreplace = $("select[name=selectaddorreplace]").val();
723 var barcodemode = $("input[name=barcodemode]:checked").val();
724 var barcodeproductqty = $("input[name=barcodeproductqty]").val();
725 var textarea = $("textarea[name=barcodelist]").val();
726 var textarray = textarea.split(/[\s,;]+/);
727 var tabproduct = [];
728 duplicatedbatchcode = [];
729 errortab1 = [];
730 errortab2 = [];
731 errortab3 = [];
732 errortab4 = [];
733
734 textarray = textarray.filter(function(value){
735 return value != "";
736 });
737 if(textarray.some((element) => element != "")){
738 $(".expectedqty").each(function(){
739 id = this.id;
740 console.log("Analyze the line "+id+" in inventory, barcodemode="+barcodemode);
741 warehouse = $("#"+id+"_warehouse").attr(\'data-ref\');
742 //console.log(warehouse);
743 productbarcode = $("#"+id+"_product").attr(\'data-barcode\');
744 //console.log(productbarcode);
745 productbatchcode = $("#"+id+"_batch").attr(\'data-batch\');
746 //console.log(productbatchcode);
747
748 if (barcodemode != "barcodeforproduct") {
749 tabproduct.forEach(product=>{
750 console.log("product.Batch="+product.Batch+" productbatchcode="+productbatchcode);
751 if(product.Batch != "" && product.Batch == productbatchcode){
752 console.log("duplicate batch code found for batch code "+productbatchcode);
753 duplicatedbatchcode.push(productbatchcode);
754 }
755 })
756 }
757 productinput = $("#"+id+"_input").val();
758 if(productinput == ""){
759 productinput = 0
760 }
761 tabproduct.push({\'Id\':id,\'Warehouse\':warehouse,\'Barcode\':productbarcode,\'Batch\':productbatchcode,\'Qty\':productinput,\'fetched\':false});
762 });
763
764 console.log("Loop on each record entered in the textarea");
765 textarray.forEach(function(element,index){
766 console.log("Process record element="+element+" id="+id);
767 var verify_batch = false;
768 var verify_barcode = false;
769 switch(barcodemode){
770 case "barcodeforautodetect":
771 verify_barcode = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"barcode",true);
772 verify_batch = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"lotserial",true);
773 break;
774 case "barcodeforproduct":
775 verify_barcode = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"barcode");
776 break;
777 case "barcodeforlotserial":
778 verify_batch = barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,"lotserial");
779 break;
780 default:
781 alert(\''.dol_escape_js($langs->trans("ErrorWrongBarcodemode")).' "\'+barcodemode+\'"\');
782 throw \''.dol_escape_js($langs->trans('ErrorWrongBarcodemode')).' "\'+barcodemode+\'"\';
783 }
784
785 if (verify_batch == false && verify_barcode == false) { /* If the 2 flags are false, not found error */
786 errortab2.push(element);
787 } else if (verify_batch == true && verify_barcode == true) { /* If the 2 flags are true, error: we don t know which one to take */
788 errortab3.push(element);
789 } else if (verify_batch == true) {
790 console.log("element="+element);
791 console.log(duplicatedbatchcode);
792 if (duplicatedbatchcode.includes(element)) {
793 errortab1.push(element);
794 }
795 }
796 });
797
798 if (Object.keys(errortab1).length < 1 && Object.keys(errortab2).length < 1 && Object.keys(errortab3).length < 1) {
799 tabproduct.forEach(product => {
800 if(product.Qty!=0){
801 console.log("We change #"+product.Id+"_input to match input in scanner box");
802 if(product.hasOwnProperty("reelqty")){
803 $.ajax({ url: \''.DOL_URL_ROOT.'/product/inventory/ajax/searchfrombarcode.php\',
804 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},
805 type: \'POST\',
806 async: false,
807 success: function(response) {
808 response = JSON.parse(response);
809 if(response.status == "success"){
810 console.log(response.message);
811 $("<input type=\'text\' value=\'"+product.Qty+"\' />")
812 .attr("id", "id_"+response.id_line+"_input")
813 .attr("name", "id_"+response.id_line)
814 .appendTo("#formrecord");
815 }else{
816 console.error(response.message);
817 }
818 },
819 error : function(output) {
820 console.error("Error on line creation function");
821 },
822 });
823 } else {
824 $("#"+product.Id+"_input").val(product.Qty);
825 }
826 }
827 });
828 jQuery("#scantoolmessage").text("'.dol_escape_js($langs->transnoentities("QtyWasAddedToTheScannedBarcode")).'\n");
829 /* document.forms["formrecord"].submit(); */
830 } else {
831 let stringerror = "";
832 if (Object.keys(errortab1).length > 0) {
833 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorSameBatchNumber')).': ";
834 errortab1.forEach(element => {
835 stringerror += (element + ", ")
836 });
837 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
838 }
839 if (Object.keys(errortab2).length > 0) {
840 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorCantFindCodeInInventory')).': ";
841 errortab2.forEach(element => {
842 stringerror += (element + ", ")
843 });
844 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
845 }
846 if (Object.keys(errortab3).length > 0) {
847 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorCodeScannedIsBothProductAndSerial')).': ";
848 errortab3.forEach(element => {
849 stringerror += (element + ", ")
850 });
851 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
852 }
853 if (Object.keys(errortab4).length > 0) {
854 stringerror += "<br>'.dol_escape_js($langs->transnoentities('ErrorBarcodeNotFoundForProductWarehouse')).': ";
855 errortab4.forEach(element => {
856 stringerror += (element + ", ")
857 });
858 stringerror = stringerror.slice(0, -2); /* Remove last ", " */
859 }
860
861 jQuery("#scantoolmessage").html(\''.dol_escape_js($langs->transnoentities("ErrorOnElementsInventory")).'\' + stringerror);
862 //alert("'.dol_escape_js($langs->trans("ErrorOnElementsInventory")).' :\n" + stringerror);
863 }
864 }
865
866 }
867
868 /* This methode is called by parent barcodescannerjs() */
869 function barcodeserialforproduct(tabproduct,index,element,barcodeproductqty,selectaddorreplace,mode,autodetect=false){
870 BarcodeIsInProduct=0;
871 newproductrow=0
872 result=false;
873 tabproduct.forEach(product => {
874 $.ajax({ url: \''.DOL_URL_ROOT.'/product/inventory/ajax/searchfrombarcode.php\',
875 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},
876 type: \'POST\',
877 async: false,
878 success: function(response) {
879 response = JSON.parse(response);
880 if (response.status == "success"){
881 console.log(response.message);
882 if(!newproductrow){
883 newproductrow = response.object;
884 }
885 }else{
886 if (mode!="lotserial" && autodetect==false && !errortab4.includes(element)){
887 errortab4.push(element);
888 console.error(response.message);
889 }
890 }
891 },
892 error : function(output) {
893 console.error("Error on barcodeserialforproduct function");
894 },
895 });
896 console.log("Product "+(index+=1)+": "+element);
897 if(mode == "barcode"){
898 testonproduct = product.Barcode
899 }else if (mode == "lotserial"){
900 testonproduct = product.Batch
901 }
902 if(testonproduct == element){
903 if(selectaddorreplace == "add"){
904 productqty = parseInt(product.Qty,10);
905 product.Qty = productqty + parseInt(barcodeproductqty,10);
906 }else if(selectaddorreplace == "replace"){
907 if(product.fetched == false){
908 product.Qty = barcodeproductqty
909 product.fetched=true
910 }else{
911 productqty = parseInt(product.Qty,10);
912 product.Qty = productqty + parseInt(barcodeproductqty,10);
913 }
914 }
915 BarcodeIsInProduct+=1;
916 }
917 })
918 if(BarcodeIsInProduct==0 && newproductrow!=0){
919 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});
920 result = true;
921 }
922 if(BarcodeIsInProduct > 0){
923 result = true;
924 }
925 return result;
926 }
927 ';
928 print '</script>';
929 }
930 include DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
931 $formother = new FormOther($db);
932 print $formother->getHTMLScannerForm("barcodescannerjs", 'all');
933}
934
935//Call method to undo changes in real qty
936print '<script>';
937print 'jQuery(document).ready(function() {
938 $("#clearqty").on("click", function() {
939 console.log("Clear all values");
940 /* disablebuttonmakemovementandclose(); */
941 jQuery(".realqty").val("");
942 jQuery(".realqty").trigger("change");
943 return false; /* disable submit */
944 });
945 $(".undochangesqty").on("click", function undochangesqty() {
946 console.log("Clear value of inventory line");
947 id = this.id;
948 id = id.split("_")[1];
949 tmpvalue = $("#id_"+id+"_input_tmp").val()
950 $("#id_"+id+"_input")[0].value = tmpvalue;
951 /* disablebuttonmakemovementandclose(); */
952 return false; /* disable submit */
953 });
954});';
955print '</script>';
956
957print '<div class="fichecenter">';
958//print '<div class="fichehalfleft">';
959print '<div class="clearboth"></div>';
960
961//print load_fiche_titre($langs->trans('Consumption'), '', '');
962
963print '<div class="div-table-responsive-no-min">';
964print '<table id="tablelines" class="noborder noshadow centpercent">';
965
966print '<tr class="liste_titre">';
967print getTitleFieldOfList($langs->trans("Warehouse"), 0, $_SERVER['PHP_SELF'], 'e.ref', '', 'id=' . $object->id . '&page=' . $page . $param, '', $sortfield, $sortorder, '', 0, '') . "\n";
968print getTitleFieldOfList($langs->trans("Product"), 0, $_SERVER['PHP_SELF'], 'p.ref', '', 'id=' . $object->id . '&page=' . $page . $param, '', $sortfield, $sortorder, '', 0, '') . "\n";
969if (isModEnabled('productbatch')) {
970 print '<td>';
971 print $langs->trans("Batch");
972 print '</td>';
973}
974if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
975 // Expected quantity = If inventory is open: Quantity currently in stock (may change if stock movement are done during the inventory)
976 print '<td class="right">'.$form->textwithpicto($langs->trans("ExpectedQty"), $langs->trans("QtyCurrentlyKnownInStock")).'</td>';
977} else {
978 // Expected quantity = If inventory is closed: Quantity we had in stock when we start the inventory.
979 print '<td class="right">'.$form->textwithpicto($langs->trans("ExpectedQty"), $langs->trans("QtyInStockWhenInventoryWasValidated")).'</td>';
980}
981if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
982 print '<td class="right">'.$langs->trans('PMPExpected').'</td>';
983 print '<td class="right">'.$langs->trans('ExpectedValuation').'</td>';
984 print '<td class="right">'.$form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp")).'</td>';
985 print '<td class="right">'.$langs->trans('PMPReal').'</td>';
986 print '<td class="right">'.$langs->trans('RealValuation').'</td>';
987} else {
988 print '<td class="right">';
989 print $form->textwithpicto($langs->trans("RealQty"), $langs->trans("InventoryRealQtyHelp"));
990 print '</td>';
991}
992if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
993 // Actions or link to stock movement
994 print '<td class="center">';
995 print '</td>';
996} else {
997 // Actions or link to stock movement
998 print '<td class="right">';
999 //print $langs->trans("StockMovement");
1000 print '</td>';
1001}
1002print '</tr>';
1003
1004// Line to add a new line in inventory
1005if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1006 print '<tr>';
1007 print '<td>';
1008 print $formproduct->selectWarehouses((GETPOSTISSET('fk_warehouse') ? GETPOSTINT('fk_warehouse') : $object->fk_warehouse), 'fk_warehouse', 'warehouseopen', 1, 0, 0, '', 0, 0, array(), 'maxwidth300');
1009 print '</td>';
1010 print '<td>';
1011 if (getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
1012 $filtertype = '';
1013 } else {
1014 $filtertype = 0;
1015 }
1016 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');
1017 print '</td>';
1018 if (isModEnabled('productbatch')) {
1019 print '<td>';
1020 print '<input type="text" name="batch" class="maxwidth100" value="'.(GETPOSTISSET('batch') ? GETPOST('batch') : '').'">';
1021 print '</td>';
1022 }
1023 print '<td class="right"></td>';
1024 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1025 print '<td class="right">';
1026 print '</td>';
1027 print '<td class="right">';
1028 print '</td>';
1029 print '<td class="right">';
1030 print '<input type="text" name="qtytoadd" class="maxwidth75" value="">';
1031 print '</td>';
1032 print '<td class="right">';
1033 print '</td>';
1034 print '<td class="right">';
1035 print '</td>';
1036 } else {
1037 print '<td class="right">';
1038 print '<input type="text" name="qtytoadd" class="maxwidth75" value="">';
1039 print '</td>';
1040 }
1041 // Actions
1042 print '<td class="center">';
1043 if ($permissiontoupdatestock) {
1044 print '<input type="submit" class="button paddingright" name="addline" value="'.$langs->trans("Add").'">';
1045 } else {
1046 print '<input type="submit" class="button paddingright" disabled="disabled" title="'.dol_escape_htmltag($langs->trans("NotEnoughPermissions")).'" name="addline" value="'.$langs->trans("Add").'">';
1047 }
1048 print '</td>';
1049 print '</tr>';
1050}
1051
1052// Request to show lines of inventory (prefilled after start/validate step)
1053$sql = 'SELECT id.rowid, id.datec as date_creation, id.tms as date_modification, id.fk_inventory, id.fk_warehouse,';
1054$sql .= ' id.fk_product, id.batch, id.qty_stock, id.qty_view, id.qty_regulated, id.fk_movement, id.pmp_real, id.pmp_expected';
1055$sql .= ' FROM ' . $db->prefix() . 'inventorydet as id';
1056$sql .= ' LEFT JOIN ' . $db->prefix() . 'product as p ON id.fk_product = p.rowid';
1057$sql .= ' LEFT JOIN ' . $db->prefix() . 'entrepot as e ON id.fk_warehouse = e.rowid';
1058$sql .= ' WHERE id.fk_inventory = ' . ((int) $object->id);
1059$sql .= $db->order($sortfield, $sortorder);
1060$sql .= $db->plimit($limit, $offset);
1061
1062$cacheOfProducts = array();
1063$cacheOfWarehouses = array();
1064
1065//$sql = '';
1066$resql = $db->query($sql);
1067if ($resql) {
1068 $num = $db->num_rows($resql);
1069
1070 if (!empty($limit != 0) || $num > $limit || $page) {
1071 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);
1072 }
1073
1074 $i = 0;
1075 $hasinput = false;
1076 $totalarray = array();
1077 while ($i < $num) {
1078 $obj = $db->fetch_object($resql);
1079
1080 if (isset($cacheOfWarehouses[$obj->fk_warehouse])) {
1081 $warehouse_static = $cacheOfWarehouses[$obj->fk_warehouse];
1082 } else {
1083 $warehouse_static = new Entrepot($db);
1084 $warehouse_static->fetch($obj->fk_warehouse);
1085
1086 $cacheOfWarehouses[$warehouse_static->id] = $warehouse_static;
1087 }
1088
1089 // Load real stock we have now
1090 $option = '';
1091 if (isset($cacheOfProducts[$obj->fk_product])) {
1092 $product_static = $cacheOfProducts[$obj->fk_product];
1093 } else {
1094 $product_static = new Product($db);
1095 $result = $product_static->fetch($obj->fk_product, '', '', '', 1, 1, 1);
1096
1097 //$option = 'nobatch';
1098 $option .= ',novirtual';
1099 $product_static->load_stock($option); // Load stock_reel + stock_warehouse.
1100
1101 $cacheOfProducts[$product_static->id] = $product_static;
1102 }
1103
1104 print '<tr class="oddeven">';
1105 print '<td id="id_'.$obj->rowid.'_warehouse" data-ref="'.dol_escape_htmltag($warehouse_static->ref).'">';
1106 print $warehouse_static->getNomUrl(1);
1107 print '</td>';
1108 print '<td id="id_'.$obj->rowid.'_product" data-ref="'.dol_escape_htmltag($product_static->ref).'" data-barcode="'.dol_escape_htmltag($product_static->barcode).'">';
1109 print $product_static->getNomUrl(1).' - '.$product_static->label;
1110 print '</td>';
1111
1112 if (isModEnabled('productbatch')) {
1113 print '<td id="id_'.$obj->rowid.'_batch" data-batch="'.dol_escape_htmltag($obj->batch).'">';
1114 $batch_static = new Productlot($db);
1115 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1116 $res = $batch_static->fetch(0, $product_static->id, $obj->batch);
1117 if ($res) {
1118 print $batch_static->getNomUrl(1);
1119 } else {
1120 print dol_escape_htmltag($obj->batch);
1121 }
1122 print '</td>';
1123 }
1124
1125 // Expected quantity = If inventory is open: Quantity currently in stock (may change if stock movement are done during the inventory)
1126 // Expected quantity = If inventory is closed: Quantity we had in stock when we start the inventory.
1127 print '<td class="right expectedqty" id="id_'.$obj->rowid.'" title="Stock viewed at last update: '.$obj->qty_stock.'">';
1128 $valuetoshow = $obj->qty_stock;
1129 // For inventory not yet close, we overwrite with the real value in stock now
1130 if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1131 if (isModEnabled('productbatch') && $product_static->hasbatch()) {
1132 $valuetoshow = $product_static->stock_warehouse[$obj->fk_warehouse]->detail_batch[$obj->batch]->qty ?? 0;
1133 } else {
1134 $valuetoshow = !empty($product_static->stock_warehouse[$obj->fk_warehouse]->real) ? $product_static->stock_warehouse[$obj->fk_warehouse]->real : 0;
1135 }
1136 }
1137 print price2num($valuetoshow, 'MS');
1138 print '<input type="hidden" name="stock_qty_'.$obj->rowid.'" value="'.$valuetoshow.'">';
1139 print '</td>';
1140
1141 // Real quantity
1142 if ($object->status == $object::STATUS_DRAFT || $object->status == $object::STATUS_VALIDATED) {
1143 $qty_view = GETPOST("id_".$obj->rowid) && price2num(GETPOST("id_".$obj->rowid), 'MS') >= 0 ? GETPOST("id_".$obj->rowid) : $obj->qty_view;
1144
1145 //if (!$hasinput && $qty_view !== null && $obj->qty_stock != $qty_view) {
1146 if ($qty_view != '') {
1147 $hasinput = true;
1148 }
1149
1150 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1151 //PMP Expected
1152 if (!empty($obj->pmp_expected)) {
1153 $pmp_expected = $obj->pmp_expected;
1154 } else {
1155 $pmp_expected = $product_static->pmp;
1156 }
1157 $pmp_valuation = $pmp_expected * $valuetoshow;
1158 print '<td class="right">';
1159 print is_null($pmp_expected) ? '' : price($pmp_expected);
1160 print '<input type="hidden" name="expectedpmp_'.$obj->rowid.'" value="'.$pmp_expected.'"/>';
1161 print '</td>';
1162 print '<td class="right">';
1163 print price($pmp_valuation);
1164 print '</td>';
1165
1166 print '<td class="right">';
1167 print '<a id="undochangesqty_'.$obj->rowid.'" href="#" class="undochangesqty reposition marginrightonly" title="'.dol_escape_htmltag($langs->trans("Clear")).'">';
1168 print img_picto('', 'eraser', 'class="opacitymedium"');
1169 print '</a>';
1170 print '<input type="text" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input" value="'.$qty_view.'">';
1171 print '</td>';
1172
1173 //PMP Real
1174 print '<td class="right">';
1175 if (!empty($obj->pmp_real) || (string) $obj->pmp_real === '0') {
1176 $pmp_real = $obj->pmp_real;
1177 } else {
1178 $pmp_real = $product_static->pmp;
1179 }
1180 $pmp_valuation_real = $pmp_real * $qty_view;
1181 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)).'">';
1182 print '</td>';
1183 print '<td class="right">';
1184 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.'">';
1185 print '</td>';
1186
1187 $totalExpectedValuation += $pmp_valuation;
1188 $totalRealValuation += $pmp_valuation_real;
1189 } else {
1190 print '<td class="right">';
1191 print '<a id="undochangesqty_'.$obj->rowid.'" href="#" class="undochangesqty reposition marginrightonly" title="'.dol_escape_htmltag($langs->trans("Clear")).'">';
1192 print img_picto('', 'eraser', 'class="opacitymedium"');
1193 print '</a>';
1194 print '<input type="text" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'" id="id_'.$obj->rowid.'_input" value="'.$qty_view.'">';
1195 print '</td>';
1196 }
1197
1198 // Picto delete line
1199 print '<td class="right">';
1200 if ($permissiontoupdatestock) {
1201 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>';
1202 }
1203 $qty_tmp = price2num(GETPOST("id_".$obj->rowid."_input_tmp"), 'MS') >= 0 ? GETPOST("id_".$obj->rowid."_input_tmp") : $qty_view;
1204 print '<input type="hidden" class="maxwidth50 right realqty" name="id_'.$obj->rowid.'_input_tmp" id="id_'.$obj->rowid.'_input_tmp" value="'.$qty_tmp.'">';
1205 print '</td>';
1206 } else {
1207 if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1208 // PMP Expected
1209 if (!empty($obj->pmp_expected)) {
1210 $pmp_expected = $obj->pmp_expected;
1211 } else {
1212 $pmp_expected = $product_static->pmp;
1213 }
1214 $pmp_valuation = $pmp_expected * $valuetoshow;
1215 print '<td class="right">';
1216 print is_null($pmp_expected) ? '' : price($pmp_expected);
1217 print '</td>';
1218 print '<td class="right">';
1219 print price($pmp_valuation);
1220 print '</td>';
1221
1222 print '<td class="right nowraponall">';
1223 print $obj->qty_view; // qty found
1224 print '</td>';
1225
1226 // PMP Real
1227 print '<td class="right">';
1228 if (!empty($obj->pmp_real)) {
1229 $pmp_real = $obj->pmp_real;
1230 } else {
1231 $pmp_real = $product_static->pmp;
1232 }
1233 $pmp_valuation_real = $pmp_real * $obj->qty_view;
1234 print is_null($pmp_real) ? '' : price($pmp_real);
1235 print '</td>';
1236 print '<td class="right">';
1237 print price($pmp_valuation_real);
1238 print '</td>';
1239 print '<td class="nowraponall right">';
1240
1241 $totalExpectedValuation += $pmp_valuation;
1242 $totalRealValuation += $pmp_valuation_real;
1243 } else {
1244 print '<td class="right nowraponall">';
1245 print $obj->qty_view; // qty found
1246 print '</td>';
1247 }
1248 print '<td>';
1249 if ($obj->fk_movement > 0) {
1250 $stockmovment = new MouvementStock($db);
1251 $stockmovment->fetch($obj->fk_movement);
1252 print $stockmovment->getNomUrl(1, 'movements');
1253 }
1254 print '</td>';
1255 }
1256 print '</tr>';
1257
1258 $i++;
1259 }
1260} else {
1262}
1263if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1264 print '<tr class="liste_total">';
1265 print '<td colspan="4">'.$langs->trans("Total").'</td>';
1266 print '<td class="right" colspan="2">'.price($totalExpectedValuation).'</td>';
1267 print '<td class="right" id="totalRealValuation" colspan="3">'.price($totalRealValuation).'</td>';
1268 print '<td></td>';
1269 print '</tr>';
1270}
1271print '</table>';
1272
1273print '</div>';
1274
1275if ($object->status == $object::STATUS_VALIDATED) {
1276 print '<center><input id="submitrecord" type="submit" class="button button-save" name="save" value="'.$langs->trans("Save").'"></center>';
1277}
1278
1279print '</div>';
1280
1281
1282// Call method to disable the button if no qty entered yet for inventory
1283/*
1284if ($object->status != $object::STATUS_VALIDATED || !$hasinput) {
1285 print '<script type="text/javascript">
1286 jQuery(document).ready(function() {
1287 console.log("Call disablebuttonmakemovementandclose because status = '.((int) $object->status).' or $hasinput = '.((int) $hasinput).'");
1288 disablebuttonmakemovementandclose();
1289 });
1290 </script>';
1291}
1292*/
1293
1294print '</form>';
1295
1296print '<script type="text/javascript">
1297 $(document).ready(function() {
1298
1299 $(".paginationnext:last").click(function(e){
1300 var form = $("#formrecord");
1301 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1302 $.ajax({
1303 url: actionURL,
1304 data: form.serialize(),
1305 cache: false,
1306 success: function(result){
1307 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page + 1).$paramwithsearch.'";
1308 }});
1309 return false;
1310 });
1311
1312
1313 $(".paginationprevious:last").click(function(e){
1314 var form = $("#formrecord");
1315 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1316 $.ajax({
1317 url: actionURL,
1318 data: form.serialize(),
1319 cache: false,
1320 success: function(result){
1321 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page - 1).$paramwithsearch.'";
1322 }});
1323 return false;
1324 });
1325
1326 $("#idbuttonmakemovementandclose").click(function(e){
1327 var form = $("#formrecord");
1328 var actionURL = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'";
1329 $.ajax({
1330 url: actionURL,
1331 type: "POST",
1332 data: form.serialize(),
1333 cache: false,
1334 success: function(result){
1335 window.location.href = "'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&page='.($page).$paramwithsearch.'&action=record";
1336 }});
1337 return false;
1338 });
1339 });
1340</script>';
1341
1342
1343if (getDolGlobalString('INVENTORY_MANAGE_REAL_PMP')) {
1344 ?>
1345<script type="text/javascript">
1346$('.realqty').on('change', function () {
1347 let realqty = $(this).closest('tr').find('.realqty').val();
1348 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1349 let realpmp = $(inputPmp).val();
1350 if (!isNaN(realqty) && !isNaN(realpmp)) {
1351 let realval = realqty * realpmp;
1352 $(this).closest('tr').find('input[name^=realvaluation]').val(realval.toFixed(2));
1353 }
1354 updateTotalValuation();
1355});
1356
1357$('input[class*=realpmp]').on('change', function () {
1358 let inputQtyReal = $(this).closest('tr').find('.realqty');
1359 let realqty = $(inputQtyReal).val();
1360 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1361 console.log(inputPmp);
1362 let realPmpClassname = $(inputPmp).attr('class').match(/[\w-]*realpmp[\w-]*/g)[0];
1363 let realpmp = $(inputPmp).val();
1364 if (!isNaN(realpmp)) {
1365 $('.'+realPmpClassname).val(realpmp); //For batch case if pmp is changed we change it everywhere it's same product and calc back everything
1366
1367 if (!isNaN(realqty)) {
1368 let realval = realqty * realpmp;
1369 $(this).closest('tr').find('input[name^=realvaluation]').val(realval.toFixed(2));
1370 }
1371 $('.realqty').trigger('change');
1372 updateTotalValuation();
1373 }
1374});
1375
1376$('input[name^=realvaluation]').on('change', function () {
1377 let inputQtyReal = $(this).closest('tr').find('.realqty');
1378 let realqty = $(inputQtyReal).val();
1379 let inputPmp = $(this).closest('tr').find('input[class*=realpmp]');
1380 let inputRealValuation = $(this).closest('tr').find('input[name^=realvaluation]');
1381 let realPmpClassname = $(inputPmp).attr('class').match(/[\w-]*realpmp[\w-]*/g)[0];
1382 let realvaluation = $(inputRealValuation).val();
1383 if (!isNaN(realvaluation) && !isNaN(realqty) && realvaluation !== '' && realqty !== '' && realqty !== 0) {
1384 let realpmp = realvaluation / realqty
1385 $('.'+realPmpClassname).val(realpmp); //For batch case if pmp is changed we change it everywhere it's same product and calc back everything
1386 $('.realqty').trigger('change');
1387 updateTotalValuation();
1388 }
1389});
1390
1391function updateTotalValuation() {
1392 let total = 0;
1393 $('input[name^=realvaluation]').each(function( index ) {
1394 let val = $(this).val();
1395 if(!isNaN(val)) total += parseFloat($(this).val());
1396 });
1397 let currencyFractionDigits = new Intl.NumberFormat('fr-FR', {
1398 style: 'currency',
1399 currency: 'EUR',
1400 }).resolvedOptions().maximumFractionDigits;
1401 $('#totalRealValuation').html(total.toLocaleString('fr-FR', {
1402 maximumFractionDigits: currencyFractionDigits
1403 }));
1404}
1405
1406
1407</script>
1408 <?php
1409}
1410
1411// End of page
1412llxFooter();
1413$db->close();
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
$totalarray
Definition list.php:497
llxFooter($comment='', $zone='private', $disabledoutputofmessages=0)
Empty footer.
Definition wrapper.php:91
if(!defined('NOREQUIRESOC')) if(!defined( 'NOREQUIRETRAN')) if(!defined('NOTOKENRENEWAL')) if(!defined( 'NOREQUIREMENU')) if(!defined('NOREQUIREHTML')) if(!defined( 'NOREQUIREAJAX')) llxHeader($head='', $title='', $help_url='', $target='', $disablejs=0, $disablehead=0, $arrayofjs='', $arrayofcss='', $morequerystring='', $morecssonbody='', $replacemainareaby='', $disablenofollow=0, $disablenoindex=0)
Empty header.
Definition wrapper.php:73
Class to manage warehouses.
Class to manage standard extra fields.
Class to manage generation of HTML components Only common components must be here.
Class to help generate other html components Only common components are here.
Class with static methods for building HTML components related to products Only components common to ...
Class for Inventory.
Class InventoryLine.
Class to manage stock movements.
Class to manage products or services.
Class with list of lots and properties.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
dol_now($mode='gmt')
Return date for now.
setEventMessages($mesg, $mesgs, $style='mesgs', $messagekey='', $noduplicate=0, $attop=0)
Set event messages in dol_events session object.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
img_delete($titlealt='default', $other='class="pictodelete"', $morecss='')
Show delete logo.
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 '.
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.
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:130
restrictedArea(User $user, $features, $object=0, $tableandshare='', $feature2='', $dbt_keyfield='fk_soc', $dbt_select='rowid', $isdraft=0, $mode=0)
Check permissions of a user to show a page and an object.