From 764170edd3cae91b20439da8774abf9d389c9e38 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:39:21 +0100 Subject: [PATCH 1/6] Implement disposal processing workflow --- DisposalProcessing.php | 760 +++++++++++++++++++++++++++ classes/Device.class.php | 12 + locale/en_US/LC_MESSAGES/openDCIM.po | 285 ++++++++++ locale/fr_FR/LC_MESSAGES/openDCIM.po | 285 ++++++++++ misc.inc.php | 3 + 5 files changed, 1345 insertions(+) create mode 100644 DisposalProcessing.php diff --git a/DisposalProcessing.php b/DisposalProcessing.php new file mode 100644 index 000000000..defa8915c --- /dev/null +++ b/DisposalProcessing.php @@ -0,0 +1,760 @@ +SiteAdmin){ + header('Location: index.php'); + exit; + } + + $subheader=__("Automated Disposal Processing"); + + if(!defined('DISPOSAL_SESSION_KEY')){ + define('DISPOSAL_SESSION_KEY','opendcim_disposal_processing'); + } + + function disposalDefaultState(){ + return array( + 'stage'=>'idle', + 'file'=>'', + 'headers'=>array(), + 'selected_column'=>-1, + 'serials'=>array(), + 'categories'=>array( + 'unknown'=>array(), + 'not_storage'=>array(), + 'storage_ready'=>array(), + 'already_disposed'=>array() + ), + 'actions'=>array( + 'unknown_ignored'=>false, + 'unknown_created'=>false, + 'not_storage_moved'=>false, + 'storage_confirmed'=>false, + 'final_processed'=>false, + 'rollback_done'=>false + ), + 'log'=>array(), + 'disposition_id'=>0, + 'operation_date'=>date('Y-m-d'), + 'completed'=>false + ); + } + + function disposalSaveState($state){ + $_SESSION[DISPOSAL_SESSION_KEY]=$state; + } + + function disposalLoadState(){ + if(isset($_SESSION[DISPOSAL_SESSION_KEY]) && is_array($_SESSION[DISPOSAL_SESSION_KEY])){ + $default=disposalDefaultState(); + $state=array_merge($default,$_SESSION[DISPOSAL_SESSION_KEY]); + foreach($default['categories'] as $key=>$value){ + if(!isset($state['categories'][$key]) || !is_array($state['categories'][$key])){ + $state['categories'][$key]=array(); + } + } + foreach($default['actions'] as $key=>$value){ + if(!isset($state['actions'][$key])){ + $state['actions'][$key]=false; + } + } + if(!isset($state['log']) || !is_array($state['log'])){ + $state['log']=array(); + } + return $state; + } + $state=disposalDefaultState(); + disposalSaveState($state); + return $state; + } + + function disposalResetState(){ + $state=disposalLoadState(); + if(isset($state['file']) && $state['file']!='' && file_exists($state['file'])){ + @unlink($state['file']); + } + $state=disposalDefaultState(); + disposalSaveState($state); + return $state; + } + + function disposalFormatLocation($device){ + $cabinetLabel=''; + $rowLabel=''; + $roomLabel=''; + if($device->Cabinet>0){ + $cab=new Cabinet(); + $cab->CabinetID=$device->Cabinet; + if($cab->GetCabinet()){ + $cabinetLabel=$cab->Location; + if($cab->CabRowID>0){ + $row=new CabRow(); + $row->CabRowID=$cab->CabRowID; + if($row->GetCabRow()){ + $rowLabel=$row->Name; + } + } + $dc=new DataCenter(); + $dc->DataCenterID=$cab->DataCenterID; + if($dc->GetDataCenter()){ + $roomLabel=$dc->Name; + } + } + }elseif($device->Cabinet==-1){ + $roomLabel=__("Storage Room"); + }elseif($device->Cabinet==0){ + $roomLabel=__("Disposed"); + } + return trim(sprintf("%s / %s / %s",$rowLabel,$roomLabel,$cabinetLabel),"/ "); + } + + function disposalBuildRecord($device){ + return array( + 'deviceid'=>$device->DeviceID, + 'label'=>$device->Label, + 'serial'=>$device->SerialNo, + 'cabinet'=>$device->Cabinet, + 'position'=>$device->Position, + 'status'=>$device->Status, + 'location'=>disposalFormatLocation($device) + ); + } + + function disposalAppendLog(&$state,$record,$operation,$actionType,$extra=array()){ + $state['log'][]=array( + 'deviceid'=>isset($record['deviceid'])?$record['deviceid']:0, + 'label'=>isset($record['label'])?$record['label']:'', + 'serial'=>isset($record['serial'])?$record['serial']:'', + 'previous_state'=>isset($extra['previous_state'])?$extra['previous_state']:array(), + 'cabinet'=>isset($record['cabinet'])?$record['cabinet']:'', + 'position'=>isset($record['position'])?$record['position']:'', + 'location'=>isset($record['location'])?$record['location']:__("N/A"), + 'timestamp'=>date('c'), + 'operation'=>$operation, + 'action_type'=>$actionType, + 'disposition_id'=>isset($extra['disposition_id'])?$extra['disposition_id']:0, + 'operation_date'=>isset($extra['operation_date'])?$extra['operation_date']:'' + ); + } + + function disposalPerformRollback(&$state,&$messages,&$errors){ + if(empty($state['log'])){ + $errors[]=__("No operations to rollback."); + return; + } + for($i=count($state['log'])-1;$i>=0;$i--){ + $entry=$state['log'][$i]; + $deviceID=intval($entry['deviceid']); + $prev=isset($entry['previous_state'])?$entry['previous_state']:array(); + switch($entry['action_type']){ + case 'move_to_storage': + case 'dispose': + if($deviceID>0){ + $dev=new Device($deviceID); + if($dev->GetDevice()){ + if(isset($prev['cabinet'])){ + $dev->Cabinet=$prev['cabinet']; + } + if(array_key_exists('position',$prev)){ + $dev->Position=$prev['position']; + } + if(isset($prev['status'])){ + $dev->Status=$prev['status']; + } + $dev->UpdateDevice(); + if($entry['action_type']=='dispose'){ + DispositionMembership::removeDevice($deviceID); + } + } + } + break; + case 'create_minimal': + if($deviceID>0){ + $dev=new Device($deviceID); + if($dev->GetDevice()){ + $dev->DeleteDevice(); + } + } + break; + default: + break; + } + } + $state['actions']['rollback_done']=true; + $messages[]=__("The rollback completed successfully."); + } + + $messages=array(); + $errors=array(); + $state=disposalLoadState(); + + if($_SERVER['REQUEST_METHOD']!='POST' && !isset($_GET['downloadlog'])){ + if($state['stage']!='idle'){ + $state=disposalResetState(); + } + } + + if(isset($_GET['downloadlog']) && !empty($state['log'])){ + header('Content-Type: text/csv; charset=UTF-8'); + header('Content-Disposition: attachment;filename="disposal-log-'.date('YmdHis').'.csv"'); + echo "DeviceID,Label,SerialNumber,PreviousCabinet,PreviousPosition,Operation,Timestamp\r\n"; + foreach($state['log'] as $entry){ + $row=array( + $entry['deviceid'], + preg_replace('/"/','""',$entry['label']), + preg_replace('/"/','""',$entry['serial']), + isset($entry['previous_state']['cabinet'])?$entry['previous_state']['cabinet']:'', + isset($entry['previous_state']['position'])?$entry['previous_state']['position']:'', + preg_replace('/"/','""',$entry['operation']), + $entry['timestamp'] + ); + echo '"'.implode('","',$row).'"'."\r\n"; + } + exit; + } + + if(isset($_POST['dp-action'])){ + $action=$_POST['dp-action']; + switch($action){ + case 'upload': + $state=disposalResetState(); + if(!isset($_FILES['disposalfile']) || $_FILES['disposalfile']['error']!=UPLOAD_ERR_OK){ + $errors[]=__("Invalid file uploaded."); + break; + } + try{ + $inputType=\PhpOffice\PhpSpreadsheet\IOFactory::identify($_FILES['disposalfile']['tmp_name']); + $reader=\PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputType); + $spreadsheet=$reader->load($_FILES['disposalfile']['tmp_name']); + }catch(Exception $e){ + $errors[]=__("The uploaded file could not be read."); + break; + } + $sheet=$spreadsheet->getSheet(0); + $highestColumn=$sheet->getHighestColumn(); + $headerRow=$sheet->rangeToArray('A1:'.$highestColumn.'1'); + $headers=(isset($headerRow[0]) && is_array($headerRow[0]))?$headerRow[0]:array(); + $tmp=tempnam(sys_get_temp_dir(),'dp_'); + @unlink($tmp); + $target=$tmp.'.xlsx'; + if(!move_uploaded_file($_FILES['disposalfile']['tmp_name'],$target)){ + $errors[]=__("Invalid file uploaded."); + break; + } + $state['file']=$target; + $state['headers']=$headers; + $state['stage']='select_column'; + $messages[]=__("File uploaded successfully. Please select the column that contains the serial numbers."); + break; + case 'select-column': + if($state['file']=='' || !file_exists($state['file'])){ + $errors[]=__("The uploaded file could not be read."); + break; + } + $columnIndex=isset($_POST['selected_column'])?intval($_POST['selected_column']):-1; + if($columnIndex<0 || $columnIndex>=count($state['headers'])){ + $errors[]=__("Please select a valid column."); + break; + } + try{ + $inputType=\PhpOffice\PhpSpreadsheet\IOFactory::identify($state['file']); + $reader=\PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputType); + $spreadsheet=$reader->load($state['file']); + }catch(Exception $e){ + $errors[]=__("The uploaded file could not be read."); + break; + } + $sheet=$spreadsheet->getSheet(0); + $highestRow=$sheet->getHighestRow(); + $serials=array(); + for($row=2;$row<=$highestRow;$row++){ + $value=$sheet->getCellByColumnAndRow($columnIndex+1,$row)->getCalculatedValue(); + $serial=trim((string)$value); + if($serial!==''){ + $serials[$serial]=$serial; + } + } + $serials=array_values($serials); + if(empty($serials)){ + $errors[]=__("No serial numbers were found in the selected column."); + break; + } + $categories=array( + 'unknown'=>array(), + 'not_storage'=>array(), + 'storage_ready'=>array(), + 'already_disposed'=>array() + ); + foreach($serials as $serial){ + $device=Device::GetDeviceBySerialNumber($serial,false); + if(!$device){ + $categories['unknown'][]=array('serial'=>$serial); + continue; + } + $record=disposalBuildRecord($device); + if($device->Cabinet>0){ + $categories['not_storage'][$device->DeviceID]=$record; + }elseif($device->Cabinet==-1){ + $categories['storage_ready'][$device->DeviceID]=$record; + }elseif($device->Cabinet==0){ + $record['status_note']=__("Already disposed / Skipped"); + $categories['already_disposed'][$device->DeviceID]=$record; + disposalAppendLog($state,$record,__("Already disposed / Skipped"),'skipped'); + }else{ + $categories['not_storage'][$device->DeviceID]=$record; + } + } + $state['serials']=$serials; + $state['categories']=$categories; + $state['selected_column']=$columnIndex; + $state['stage']='ready'; + $messages[]=__("Serial numbers ready for processing."); + break; + case 'ignore-unknown': + if($state['actions']['unknown_ignored']){ + break; + } + if(!empty($state['categories']['unknown'])){ + foreach($state['categories']['unknown'] as $entry){ + $record=array('serial'=>$entry['serial'],'location'=>__("N/A")); + disposalAppendLog($state,$record,sprintf(__("Ignored unknown serial number %s"),$entry['serial']),'ignored'); + } + $state['categories']['unknown']=array(); + $state['actions']['unknown_ignored']=true; + $messages[]=__("Unknown serial numbers have been ignored."); + } + break; + case 'create-minimal': + if($state['actions']['unknown_created']){ + break; + } + if(!empty($state['categories']['unknown'])){ + foreach($state['categories']['unknown'] as $entry){ + $serial=$entry['serial']; + $device=new Device(); + $device->Label=$serial; + $device->SerialNo=$serial; + $device->Cabinet=-1; + $device->Position=0; + $device->Height=0; + $device->Ports=0; + $device->NominalWatts=0; + $device->PowerSupplyCount=0; + $device->FirstPortNum=1; + $device->TemplateID=0; + $device->DeviceType='Server'; + $device->ChassisSlots=0; + $device->RearChassisSlots=0; + $device->ParentDevice=0; + $device->Status='Reserved'; + $device->AuditStamp=date('Y-m-d'); + $device->MfgDate=date('Y-m-d'); + $device->InstallDate=date('Y-m-d'); + $device->WarrantyExpire=date('Y-m-d'); + if($device->CreateDevice()){ + $record=disposalBuildRecord($device); + $state['categories']['storage_ready'][$device->DeviceID]=$record; + disposalAppendLog($state,$record,__("Created minimal storage record"),'create_minimal',array('previous_state'=>array('created'=>true))); + }else{ + $errors[]=sprintf(__("Failed to create device for serial number %s."),$serial); + } + } + $state['categories']['unknown']=array(); + $state['actions']['unknown_created']=true; + $messages[]=__("Minimal device entries created successfully."); + } + break; + case 'move-to-storage': + if($state['actions']['not_storage_moved']){ + break; + } + $selected=isset($_POST['deviceids'])?$_POST['deviceids']:array(); + if(!is_array($selected) || empty($selected)){ + $errors[]=__("Please select at least one device to move."); + break; + } + foreach($selected as $deviceID){ + $deviceID=intval($deviceID); + if(!isset($state['categories']['not_storage'][$deviceID])){ + continue; + } + $record=$state['categories']['not_storage'][$deviceID]; + $dev=new Device($deviceID); + if($dev->GetDevice()){ + $prevState=array('cabinet'=>$dev->Cabinet,'position'=>$dev->Position,'status'=>$dev->Status); + $dev->MoveToStorage(); + $updated=disposalBuildRecord($dev); + $state['categories']['storage_ready'][$deviceID]=$updated; + disposalAppendLog($state,$record,__("Moved to Storage Room"),'move_to_storage',array('previous_state'=>$prevState)); + } + unset($state['categories']['not_storage'][$deviceID]); + } + $state['actions']['not_storage_moved']=true; + $messages[]=__("Devices moved to the Storage Room."); + break; + case 'confirm-storage': + $state['actions']['storage_confirmed']=true; + $messages[]=__("Storage Room devices confirmed. You may proceed with the final processing."); + break; + case 'start-processing': + if($state['actions']['final_processed']){ + break; + } + if(!$state['actions']['storage_confirmed']){ + $errors[]=__("Please confirm the Storage Room devices first."); + break; + } + if(empty($state['categories']['storage_ready'])){ + $errors[]=__("No devices are ready in the Storage Room."); + break; + } + $dispID=isset($_POST['dispositionid'])?intval($_POST['dispositionid']):0; + $dispList=Disposition::getDisposition($dispID); + if(empty($dispList)){ + $errors[]=__("Please select a valid disposal method."); + break; + } + $operationDate=isset($_POST['operation_date'])?$_POST['operation_date']:''; + $dateCheck=DateTime::createFromFormat('Y-m-d',$operationDate); + if(!$dateCheck){ + $errors[]=__("Please provide a valid operation date."); + break; + } + global $dbh; + foreach($state['categories']['storage_ready'] as $deviceID=>$record){ + $dev=new Device($deviceID); + if($dev->GetDevice()){ + $prevState=array('cabinet'=>$dev->Cabinet,'position'=>$dev->Position,'status'=>$dev->Status); + $dev->Dispose($dispID); + $st=$dbh->prepare("UPDATE fac_DispositionMembership SET DispositionDate=:DispositionDate WHERE DeviceID=:DeviceID"); + $st->execute(array(":DispositionDate"=>$operationDate,":DeviceID"=>$deviceID)); + disposalAppendLog($state,$record,__("Disposed from Storage Room"),'dispose',array('previous_state'=>$prevState,'disposition_id'=>$dispID,'operation_date'=>$operationDate)); + } + } + $state['categories']['storage_ready']=array(); + $state['actions']['final_processed']=true; + $state['completed']=true; + $state['disposition_id']=$dispID; + $state['operation_date']=$operationDate; + $messages[]=__("Devices disposed successfully."); + break; + case 'rollback': + if($state['actions']['rollback_done']){ + break; + } + disposalPerformRollback($state,$messages,$errors); + break; + case 'complete': + $state=disposalResetState(); + $messages[]=__("Process completed."); + break; + case 'cancel': + $state=disposalResetState(); + $messages[]=__("Operation cancelled."); + break; + default: + break; + } + disposalSaveState($state); + } + + $dList=Disposition::getDisposition(); +?> + + + + + + <?php echo __("Automated Disposal Processing"); ?> + + + + + + + + +
+ +
+
+
+ +
+
+
+ %s
',htmlspecialchars($msg)); + } + foreach($messages as $msg){ + printf('
%s
',htmlspecialchars($msg)); + } + ?> +
+
+

+ +
+ +
+ + +
+
+
+ +
+ +
+ + +
+
+
+
+ + +
+ +
+
+
+ +
+

+
+
+
+
+
+ '.__("No records found.").'
'; + }else{ + foreach($state['categories']['unknown'] as $entry){ + printf('
%s
',htmlspecialchars($entry['serial'])); + } + } + ?> +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+

+
+ +
+
+
+
+
+
+
+
+ '.__("No records found.").'
'; + }else{ + foreach($state['categories']['not_storage'] as $record){ + printf('
%s
%s
%s
%s
', + $record['deviceid'], + $record['deviceid'], + htmlspecialchars($record['label']), + htmlspecialchars($record['serial']), + htmlspecialchars($record['location']) + ); + } + } + ?> +
+
+ +
+ +
+ + +
+
+
+
+
+

+
+
+
+
+
+
+
+
+ '.__("No records found.").'
'; + }else{ + foreach($state['categories']['storage_ready'] as $record){ + printf('
%s
%s
%s
%s
%s
', + $record['deviceid'], + htmlspecialchars($record['label']), + htmlspecialchars($record['serial']), + htmlspecialchars($record['location']), + htmlspecialchars($record['position']) + ); + } + } + ?> +
+
+ + +
+
+ + +
+ +
+
+
+

+

+
+
+
+
+
+
+
+
+ '.__("No records found.").'
'; + }else{ + foreach($state['categories']['already_disposed'] as $record){ + $statusNote=isset($record['status_note'])?$record['status_note']:__("Already disposed / Skipped"); + printf('
%s
%s
%s
%s
%s
', + $record['deviceid'], + htmlspecialchars($record['label']), + htmlspecialchars($record['serial']), + htmlspecialchars($statusNote), + htmlspecialchars($record['location']) + ); + } + } + ?> +
+ +
+
+

+
+ +
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+

+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + diff --git a/classes/Device.class.php b/classes/Device.class.php index a1f76c262..20023a6a6 100644 --- a/classes/Device.class.php +++ b/classes/Device.class.php @@ -1526,6 +1526,18 @@ function SearchDevicebySerialNo(){ return $deviceList; } + static function GetDeviceBySerialNumber($serial,$filterrights=true){ + global $dbh; + + $serial=sanitize($serial); + $st=$dbh->prepare("SELECT * FROM fac_Device WHERE SerialNo=:SerialNo LIMIT 1"); + $st->execute(array(":SerialNo"=>$serial)); + if(($row=$st->fetch(PDO::FETCH_ASSOC))!==false){ + return Device::RowToObject($row,$filterrights); + } + return null; + } + function SearchDevicebyAssetTag(){ global $dbh; diff --git a/locale/en_US/LC_MESSAGES/openDCIM.po b/locale/en_US/LC_MESSAGES/openDCIM.po index 685cf18ee..957a1a81c 100644 --- a/locale/en_US/LC_MESSAGES/openDCIM.po +++ b/locale/en_US/LC_MESSAGES/openDCIM.po @@ -6136,3 +6136,288 @@ msgstr "First port detection failed, please report to openDCIM developers" #~ msgid "People resource for UserID=%1$s updated successfully." #~ msgstr "People resource for UserID=%1$s updated successfully." + +#: DisposalProcessing.php +msgid "Automated Disposal Processing" +msgstr "Automated Disposal Processing" + + +#: DisposalProcessing.php +msgid "N/A" +msgstr "N/A" + + +#: DisposalProcessing.php +msgid "Invalid file uploaded." +msgstr "Invalid file uploaded." + + +#: DisposalProcessing.php +msgid "The uploaded file could not be read." +msgstr "The uploaded file could not be read." + + +#: DisposalProcessing.php +msgid "File uploaded successfully. Please select the column that contains the serial numbers." +msgstr "File uploaded successfully. Please select the column that contains the serial numbers." + + +#: DisposalProcessing.php +msgid "Please select a valid column." +msgstr "Please select a valid column." + + +#: DisposalProcessing.php +msgid "No serial numbers were found in the selected column." +msgstr "No serial numbers were found in the selected column." + + +#: DisposalProcessing.php +msgid "Serial numbers ready for processing." +msgstr "Serial numbers ready for processing." + + +#: DisposalProcessing.php +msgid "Already disposed / Skipped" +msgstr "Already disposed / Skipped" + + +#: DisposalProcessing.php +msgid "Unknown serial numbers have been ignored." +msgstr "Unknown serial numbers have been ignored." + + +#: DisposalProcessing.php +msgid "Ignored unknown serial number %s" +msgstr "Ignored unknown serial number %s" + + +#: DisposalProcessing.php +msgid "Created minimal storage record" +msgstr "Created minimal storage record" + + +#: DisposalProcessing.php +msgid "Failed to create device for serial number %s." +msgstr "Failed to create device for serial number %s." + + +#: DisposalProcessing.php +msgid "Minimal device entries created successfully." +msgstr "Minimal device entries created successfully." + + +#: DisposalProcessing.php +msgid "Please select at least one device to move." +msgstr "Please select at least one device to move." + + +#: DisposalProcessing.php +msgid "Moved to Storage Room" +msgstr "Moved to Storage Room" + + +#: DisposalProcessing.php +msgid "Devices moved to the Storage Room." +msgstr "Devices moved to the Storage Room." + + +#: DisposalProcessing.php +msgid "Storage Room devices confirmed. You may proceed with the final processing." +msgstr "Storage Room devices confirmed. You may proceed with the final processing." + + +#: DisposalProcessing.php +msgid "Please confirm the Storage Room devices first." +msgstr "Please confirm the Storage Room devices first." + + +#: DisposalProcessing.php +msgid "No devices are ready in the Storage Room." +msgstr "No devices are ready in the Storage Room." + + +#: DisposalProcessing.php +msgid "Please select a valid disposal method." +msgstr "Please select a valid disposal method." + + +#: DisposalProcessing.php +msgid "Please provide a valid operation date." +msgstr "Please provide a valid operation date." + + +#: DisposalProcessing.php +msgid "Disposed from Storage Room" +msgstr "Disposed from Storage Room" + + +#: DisposalProcessing.php +msgid "Devices disposed successfully." +msgstr "Devices disposed successfully." + + +#: DisposalProcessing.php +msgid "Process completed." +msgstr "Process completed." + + +#: DisposalProcessing.php +msgid "Operation cancelled." +msgstr "Operation cancelled." + + +#: DisposalProcessing.php +msgid "Import disposal spreadsheet" +msgstr "Import disposal spreadsheet" + + +#: DisposalProcessing.php +msgid "Upload Excel File" +msgstr "Upload Excel File" + + +#: DisposalProcessing.php +msgid "Start Import" +msgstr "Start Import" + + +#: DisposalProcessing.php +msgid "Select Serial Number Column" +msgstr "Select Serial Number Column" + + +#: DisposalProcessing.php +msgid "Continue" +msgstr "Continue" + + +#: DisposalProcessing.php +msgid "Cancel operation" +msgstr "Cancel operation" + + +#: DisposalProcessing.php +msgid "Category 1: Unknown serial numbers" +msgstr "Category 1: Unknown serial numbers" + + +#: DisposalProcessing.php +msgid "Proceed without processing unknown SN" +msgstr "Proceed without processing unknown SN" + + +#: DisposalProcessing.php +msgid "Create minimal device entries automatically" +msgstr "Create minimal device entries automatically" + + +#: DisposalProcessing.php +msgid "Category 2: Devices not in the Storage Room" +msgstr "Category 2: Devices not in the Storage Room" + + +#: DisposalProcessing.php +msgid "Move selected devices to Storage Room" +msgstr "Move selected devices to Storage Room" + + +#: DisposalProcessing.php +msgid "Category 3: Devices ready in the Storage Room" +msgstr "Category 3: Devices ready in the Storage Room" + + +#: DisposalProcessing.php +msgid "Proceed with disposal method" +msgstr "Proceed with disposal method" + + +#: DisposalProcessing.php +msgid "Category 4: Already processed" +msgstr "Category 4: Already processed" + + +#: DisposalProcessing.php +msgid "Items already processed will be skipped." +msgstr "Items already processed will be skipped." + + +#: DisposalProcessing.php +msgid "Final disposal method" +msgstr "Final disposal method" + + +#: DisposalProcessing.php +msgid "Select disposition method" +msgstr "Select disposition method" + + +#: DisposalProcessing.php +msgid "Operation date" +msgstr "Operation date" + + +#: DisposalProcessing.php +msgid "Start Processing" +msgstr "Start Processing" + + +#: DisposalProcessing.php +msgid "Processing log" +msgstr "Processing log" + + +#: DisposalProcessing.php +msgid "Operation" +msgstr "Operation" + + +#: DisposalProcessing.php +msgid "Timestamp" +msgstr "Timestamp" + + +#: DisposalProcessing.php +msgid "Download full log" +msgstr "Download full log" + + +#: DisposalProcessing.php +msgid "Undo last operation" +msgstr "Undo last operation" + + +#: DisposalProcessing.php +msgid "Process Completed" +msgstr "Process Completed" + + +#: DisposalProcessing.php +msgid "Before starting this automated disposal workflow, please perform a full database backup." +msgstr "Before starting this automated disposal workflow, please perform a full database backup." + + +#: DisposalProcessing.php +msgid "No records found." +msgstr "No records found." + + +#: DisposalProcessing.php +msgid "Process Scrap" +msgstr "Process Scrap" + + +#: DisposalProcessing.php +msgid "No operations to rollback." +msgstr "No operations to rollback." + + +#: DisposalProcessing.php +msgid "The rollback completed successfully." +msgstr "The rollback completed successfully." + + +#: DisposalProcessing.php +msgid "Please select the column that contains the serial numbers." +msgstr "Please select the column that contains the serial numbers." + diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index b13cff3e8..65a1dfa21 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6264,3 +6264,288 @@ msgstr "" #~ msgid "MailTo" #~ msgstr "Envoyé à " + +#: DisposalProcessing.php +msgid "Automated Disposal Processing" +msgstr "Traitement automatisé du rebut" + + +#: DisposalProcessing.php +msgid "N/A" +msgstr "N/D" + + +#: DisposalProcessing.php +msgid "Invalid file uploaded." +msgstr "Fichier importé invalide." + + +#: DisposalProcessing.php +msgid "The uploaded file could not be read." +msgstr "Le fichier importé n'a pas pu être lu." + + +#: DisposalProcessing.php +msgid "File uploaded successfully. Please select the column that contains the serial numbers." +msgstr "Fichier importé avec succès. Veuillez sélectionner la colonne contenant les numéros de série." + + +#: DisposalProcessing.php +msgid "Please select a valid column." +msgstr "Veuillez sélectionner une colonne valide." + + +#: DisposalProcessing.php +msgid "No serial numbers were found in the selected column." +msgstr "Aucun numéro de série trouvé dans la colonne sélectionnée." + + +#: DisposalProcessing.php +msgid "Serial numbers ready for processing." +msgstr "Numéros de série prêts pour le traitement." + + +#: DisposalProcessing.php +msgid "Already disposed / Skipped" +msgstr "Déjà traité / Ignoré" + + +#: DisposalProcessing.php +msgid "Unknown serial numbers have been ignored." +msgstr "Les numéros de série inconnus ont été ignorés." + + +#: DisposalProcessing.php +msgid "Ignored unknown serial number %s" +msgstr "Numéro de série inconnu %s ignoré" + + +#: DisposalProcessing.php +msgid "Created minimal storage record" +msgstr "Enregistrement minimal de stockage créé" + + +#: DisposalProcessing.php +msgid "Failed to create device for serial number %s." +msgstr "Impossible de créer l'équipement pour le numéro de série %s." + + +#: DisposalProcessing.php +msgid "Minimal device entries created successfully." +msgstr "Les enregistrements minimaux ont été créés avec succès." + + +#: DisposalProcessing.php +msgid "Please select at least one device to move." +msgstr "Veuillez sélectionner au moins un équipement à déplacer." + + +#: DisposalProcessing.php +msgid "Moved to Storage Room" +msgstr "Déplacé vers la salle de stockage" + + +#: DisposalProcessing.php +msgid "Devices moved to the Storage Room." +msgstr "Équipements déplacés vers la salle de stockage." + + +#: DisposalProcessing.php +msgid "Storage Room devices confirmed. You may proceed with the final processing." +msgstr "Équipements de la salle de stockage confirmés. Vous pouvez poursuivre le traitement final." + + +#: DisposalProcessing.php +msgid "Please confirm the Storage Room devices first." +msgstr "Veuillez d'abord confirmer les équipements de la salle de stockage." + + +#: DisposalProcessing.php +msgid "No devices are ready in the Storage Room." +msgstr "Aucun équipement prêt dans la salle de stockage." + + +#: DisposalProcessing.php +msgid "Please select a valid disposal method." +msgstr "Veuillez sélectionner une méthode de sortie valide." + + +#: DisposalProcessing.php +msgid "Please provide a valid operation date." +msgstr "Veuillez fournir une date d'opération valide." + + +#: DisposalProcessing.php +msgid "Disposed from Storage Room" +msgstr "Sorti de la salle de stockage" + + +#: DisposalProcessing.php +msgid "Devices disposed successfully." +msgstr "Équipements traités avec succès." + + +#: DisposalProcessing.php +msgid "Process completed." +msgstr "Processus terminé." + + +#: DisposalProcessing.php +msgid "Operation cancelled." +msgstr "Opération annulée." + + +#: DisposalProcessing.php +msgid "Import disposal spreadsheet" +msgstr "Importer la feuille de calcul de rebut" + + +#: DisposalProcessing.php +msgid "Upload Excel File" +msgstr "Importer un fichier Excel" + + +#: DisposalProcessing.php +msgid "Start Import" +msgstr "Démarrer l'import" + + +#: DisposalProcessing.php +msgid "Select Serial Number Column" +msgstr "Sélectionner la colonne des numéros de série" + + +#: DisposalProcessing.php +msgid "Continue" +msgstr "Continuer" + + +#: DisposalProcessing.php +msgid "Cancel operation" +msgstr "Annuler l'opération" + + +#: DisposalProcessing.php +msgid "Category 1: Unknown serial numbers" +msgstr "Catégorie 1 : Numéros de série inconnus" + + +#: DisposalProcessing.php +msgid "Proceed without processing unknown SN" +msgstr "Continuer sans traiter les numéros de série inconnus" + + +#: DisposalProcessing.php +msgid "Create minimal device entries automatically" +msgstr "Créer automatiquement des enregistrements minimaux" + + +#: DisposalProcessing.php +msgid "Category 2: Devices not in the Storage Room" +msgstr "Catégorie 2 : Équipements hors de la salle de stockage" + + +#: DisposalProcessing.php +msgid "Move selected devices to Storage Room" +msgstr "Déplacer les équipements sélectionnés vers la salle de stockage" + + +#: DisposalProcessing.php +msgid "Category 3: Devices ready in the Storage Room" +msgstr "Catégorie 3 : Équipements prêts dans la salle de stockage" + + +#: DisposalProcessing.php +msgid "Proceed with disposal method" +msgstr "Continuer avec la méthode de sortie" + + +#: DisposalProcessing.php +msgid "Category 4: Already processed" +msgstr "Catégorie 4 : Déjà traités" + + +#: DisposalProcessing.php +msgid "Items already processed will be skipped." +msgstr "Les éléments déjà traités seront ignorés." + + +#: DisposalProcessing.php +msgid "Final disposal method" +msgstr "Méthode de sortie finale" + + +#: DisposalProcessing.php +msgid "Select disposition method" +msgstr "Sélectionner la méthode de sortie" + + +#: DisposalProcessing.php +msgid "Operation date" +msgstr "Date d'opération" + + +#: DisposalProcessing.php +msgid "Start Processing" +msgstr "Démarrer le traitement" + + +#: DisposalProcessing.php +msgid "Processing log" +msgstr "Journal de traitement" + + +#: DisposalProcessing.php +msgid "Operation" +msgstr "Opération" + + +#: DisposalProcessing.php +msgid "Timestamp" +msgstr "Horodatage" + + +#: DisposalProcessing.php +msgid "Download full log" +msgstr "Télécharger le journal complet" + + +#: DisposalProcessing.php +msgid "Undo last operation" +msgstr "Annuler la dernière opération" + + +#: DisposalProcessing.php +msgid "Process Completed" +msgstr "Processus terminé" + + +#: DisposalProcessing.php +msgid "Before starting this automated disposal workflow, please perform a full database backup." +msgstr "Avant de démarrer ce processus automatisé de rebut, veuillez effectuer une sauvegarde complète de la base de données." + + +#: DisposalProcessing.php +msgid "No records found." +msgstr "Aucun enregistrement trouvé." + + +#: DisposalProcessing.php +msgid "Process Scrap" +msgstr "Traitement du rebut" + + +#: DisposalProcessing.php +msgid "No operations to rollback." +msgstr "Aucune opération à annuler." + + +#: DisposalProcessing.php +msgid "The rollback completed successfully." +msgstr "Le retour arrière a été effectué avec succès." + + +#: DisposalProcessing.php +msgid "Please select the column that contains the serial numbers." +msgstr "Veuillez sélectionner la colonne contenant les numéros de série." + diff --git a/misc.inc.php b/misc.inc.php index 4079fc543..22d608446 100644 --- a/misc.inc.php +++ b/misc.inc.php @@ -1009,6 +1009,9 @@ function buildnavmenu($ma,&$tl){ $wamenu[__("Bulk Importer")][]=''.__("Import Network Connections").''; $wamenu[__("Bulk Importer")][]=''.__("Import Power Connections").''; $wamenu[__("Bulk Importer")][]=''.__("Process Bulk Moves").''; + if($person->SiteAdmin){ + $wamenu[__("Bulk Importer")][]=''.__("Process Scrap").''; + } } if ( $person->SiteAdmin ) { $samenu[__("Template Management")][]=''.__("Edit Manufacturers").''; From 7e45580c2bdf095e8f478ccfbc8a4b71a377c5e9 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:44:18 +0100 Subject: [PATCH 2/6] Document disposal processing workflow --- README_DisposalProcessing.md | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 README_DisposalProcessing.md diff --git a/README_DisposalProcessing.md b/README_DisposalProcessing.md new file mode 100644 index 000000000..a034580d5 --- /dev/null +++ b/README_DisposalProcessing.md @@ -0,0 +1,47 @@ +# Disposal Processing Workflow + +Ce document décrit la nouvelle page `DisposalProcessing.php`, disponible via le menu **Bulk Importer > Process Scrap**. Elle est réservée aux administrateurs de site et permet de traiter les équipements sortis en rebut sur la base d’un fichier Excel fourni par la logistique. + +## Fichier attendu + +- **Format** : Excel (XLSX/XLS) unique par opération. +- **Origine** : fichier préparé par l’équipe logistique listant les équipements planifiés pour destruction/recyclage. +- **Colonne pivot** : `Serial/No` (ou équivalent). Cette colonne est obligatoire car elle identifie de façon unique chaque équipement dans OpenDCIM. Les autres colonnes peuvent être présentes mais ne sont pas utilisées par l’outil. + +> Conseil : vérifiez que tous les numéros de série sont bien renseignés dans OpenDCIM avant l’import, afin de limiter le nombre de créations minimales. + +## Étapes du workflow + +1. **Téléversement du fichier** + - Charger le fichier Excel via le formulaire. + - L’outil lit la première ligne pour détecter les entêtes. +2. **Sélection de la colonne Serial Number** + - Choisir la colonne qui contient les numéros de série (ex : `Serial/No`). + - Les numéros détectés sont dédupliqués avant l’analyse. +3. **Analyse & catégorisation** + Les équipements sont classés dans quatre catégories : + - **Catégorie 1 – Unknown serial numbers** : numéros non trouvés en base. + - **Catégorie 2 – Devices not in the Storage Room** : SN trouvés mais toujours installés (Cabinet > 0). + - **Catégorie 3 – Devices ready in the Storage Room** : SN déjà en Storage Room (Cabinet = -1). + - **Catégorie 4 – Already processed** : SN déjà sortis (Cabinet = 0). Affichés en gris et ignorés pour la suite. +4. **Actions par catégorie** + - **Catégorie 1** : ignorer les SN inconnus ou créer automatiquement un équipement minimal (Label = SN, SerialNo = SN, Cabinet = -1). + - **Catégorie 2** : sélectionner les équipements repérés et déclencher le déplacement automatique vers la Storage Room. + - **Catégorie 3** : confirmer la liste et choisir la méthode de sortie (fac_Disposition) + date d’opération. + - **Catégorie 4** : consultatif, aucune action. +5. **Traitement final** + - Sélectionner la méthode de disposition et la date, puis cliquer sur **Start Processing**. + - L’outil appelle `Device::Dispose()` pour chaque équipement confirmé et enregistre les actions dans un journal. +6. **Journal & rollback** + - Le journal peut être téléchargé au format CSV. + - Après traitement, un bouton **Undo last operation** permet de restaurer les cabinets/positions et de supprimer les entrées fac_DispositionMembership ajoutées par l’opération. +7. **Réinitialisation** + - Le workflow conserve l’état tant que vous restez sur la page. Un simple rechargement (GET) réinitialise l’assistant pour un nouveau fichier. + +## Bonnes pratiques + +- Vérifier que tous les numéros de série du fichier logistique sont présents dans OpenDCIM avant import ; sinon, utiliser l’option de création minimale. +- Effectuer une sauvegarde complète de la base avant de lancer le traitement automatisé (message d’avertissement affiché en haut de page). +- Télécharger le journal après chaque session pour conserver une trace des opérations. + +En cas de question ou d’évolution souhaitée, référez-vous à la page `DisposalProcessing.php` et aux traductions associées (`openDCIM.po`). From f8183bd73418bf9caa80daf06e7e509a7c723d02 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:47:18 +0100 Subject: [PATCH 3/6] Document disposal processing workflow --- README_DisposalProcessing.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README_DisposalProcessing.md b/README_DisposalProcessing.md index a034580d5..1f47a53b9 100644 --- a/README_DisposalProcessing.md +++ b/README_DisposalProcessing.md @@ -9,6 +9,7 @@ Ce document décrit la nouvelle page `DisposalProcessing.php`, disponible via le - **Colonne pivot** : `Serial/No` (ou équivalent). Cette colonne est obligatoire car elle identifie de façon unique chaque équipement dans OpenDCIM. Les autres colonnes peuvent être présentes mais ne sont pas utilisées par l’outil. > Conseil : vérifiez que tous les numéros de série sont bien renseignés dans OpenDCIM avant l’import, afin de limiter le nombre de créations minimales. +il y a une fonction qui control si les SN sont présent dans opendcim. ## Étapes du workflow @@ -16,7 +17,7 @@ Ce document décrit la nouvelle page `DisposalProcessing.php`, disponible via le - Charger le fichier Excel via le formulaire. - L’outil lit la première ligne pour détecter les entêtes. 2. **Sélection de la colonne Serial Number** - - Choisir la colonne qui contient les numéros de série (ex : `Serial/No`). + - Choisir la colonne qui contient les numéros de série (ex : `Serial/No` ou `S/N`). - Les numéros détectés sont dédupliqués avant l’analyse. 3. **Analyse & catégorisation** Les équipements sont classés dans quatre catégories : @@ -44,4 +45,4 @@ Ce document décrit la nouvelle page `DisposalProcessing.php`, disponible via le - Effectuer une sauvegarde complète de la base avant de lancer le traitement automatisé (message d’avertissement affiché en haut de page). - Télécharger le journal après chaque session pour conserver une trace des opérations. -En cas de question ou d’évolution souhaitée, référez-vous à la page `DisposalProcessing.php` et aux traductions associées (`openDCIM.po`). +En cas de question ou d’évolution souhaitée, référez-vous à la page `DisposalProcessing.php` et aux traductions associées (`openDCIM.po`). \ No newline at end of file From 6af74ab07d2e302970239ed62c03cad5010c458e Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:51:20 +0100 Subject: [PATCH 4/6] Document disposal processing workflow --- README_DisposalProcessing.md | 105 ++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/README_DisposalProcessing.md b/README_DisposalProcessing.md index 1f47a53b9..dc91a863e 100644 --- a/README_DisposalProcessing.md +++ b/README_DisposalProcessing.md @@ -1,48 +1,61 @@ # Disposal Processing Workflow -Ce document décrit la nouvelle page `DisposalProcessing.php`, disponible via le menu **Bulk Importer > Process Scrap**. Elle est réservée aux administrateurs de site et permet de traiter les équipements sortis en rebut sur la base d’un fichier Excel fourni par la logistique. - -## Fichier attendu - -- **Format** : Excel (XLSX/XLS) unique par opération. -- **Origine** : fichier préparé par l’équipe logistique listant les équipements planifiés pour destruction/recyclage. -- **Colonne pivot** : `Serial/No` (ou équivalent). Cette colonne est obligatoire car elle identifie de façon unique chaque équipement dans OpenDCIM. Les autres colonnes peuvent être présentes mais ne sont pas utilisées par l’outil. - -> Conseil : vérifiez que tous les numéros de série sont bien renseignés dans OpenDCIM avant l’import, afin de limiter le nombre de créations minimales. -il y a une fonction qui control si les SN sont présent dans opendcim. - -## Étapes du workflow - -1. **Téléversement du fichier** - - Charger le fichier Excel via le formulaire. - - L’outil lit la première ligne pour détecter les entêtes. -2. **Sélection de la colonne Serial Number** - - Choisir la colonne qui contient les numéros de série (ex : `Serial/No` ou `S/N`). - - Les numéros détectés sont dédupliqués avant l’analyse. -3. **Analyse & catégorisation** - Les équipements sont classés dans quatre catégories : - - **Catégorie 1 – Unknown serial numbers** : numéros non trouvés en base. - - **Catégorie 2 – Devices not in the Storage Room** : SN trouvés mais toujours installés (Cabinet > 0). - - **Catégorie 3 – Devices ready in the Storage Room** : SN déjà en Storage Room (Cabinet = -1). - - **Catégorie 4 – Already processed** : SN déjà sortis (Cabinet = 0). Affichés en gris et ignorés pour la suite. -4. **Actions par catégorie** - - **Catégorie 1** : ignorer les SN inconnus ou créer automatiquement un équipement minimal (Label = SN, SerialNo = SN, Cabinet = -1). - - **Catégorie 2** : sélectionner les équipements repérés et déclencher le déplacement automatique vers la Storage Room. - - **Catégorie 3** : confirmer la liste et choisir la méthode de sortie (fac_Disposition) + date d’opération. - - **Catégorie 4** : consultatif, aucune action. -5. **Traitement final** - - Sélectionner la méthode de disposition et la date, puis cliquer sur **Start Processing**. - - L’outil appelle `Device::Dispose()` pour chaque équipement confirmé et enregistre les actions dans un journal. -6. **Journal & rollback** - - Le journal peut être téléchargé au format CSV. - - Après traitement, un bouton **Undo last operation** permet de restaurer les cabinets/positions et de supprimer les entrées fac_DispositionMembership ajoutées par l’opération. -7. **Réinitialisation** - - Le workflow conserve l’état tant que vous restez sur la page. Un simple rechargement (GET) réinitialise l’assistant pour un nouveau fichier. - -## Bonnes pratiques - -- Vérifier que tous les numéros de série du fichier logistique sont présents dans OpenDCIM avant import ; sinon, utiliser l’option de création minimale. -- Effectuer une sauvegarde complète de la base avant de lancer le traitement automatisé (message d’avertissement affiché en haut de page). -- Télécharger le journal après chaque session pour conserver une trace des opérations. - -En cas de question ou d’évolution souhaitée, référez-vous à la page `DisposalProcessing.php` et aux traductions associées (`openDCIM.po`). \ No newline at end of file +## Overview (English) +The `DisposalProcessing.php` page, available under **Bulk Importer > Process Scrap**, guides Site Administrators through an automated disposal workflow using the logistics Excel file. It imports serial numbers, classifies devices into four categories, offers per-category actions, and records every step for auditing/rollback. + +## Vue d’ensemble (Français) +La page `DisposalProcessing.php`, accessible via **Bulk Importer > Process Scrap**, accompagne les administrateurs de site dans un workflow automatisé de traitement du rebut à partir du fichier Excel logistique. Elle importe les numéros de série, classe les équipements en quatre catégories, propose les actions adaptées et journalise chaque étape avec option de rollback. + +--- + +## Expected File (EN) +- **Format**: Excel XLSX/XLS (one per run). +- **Source**: Logistics spreadsheet listing devices slated for disposal. +- **Pivot Column**: `Serial/No` (or similar). Only this column is used—others are ignored. +- Recommendation: ensure serials exist in OpenDCIM to minimize minimal-entry creation. + +## Fichier attendu (FR) +- **Format** : Excel XLSX/XLS (un par session). +- **Origine** : Fichier logistique listant les équipements destinés à la destruction/recyclage. +- **Colonne pivot** : `Serial/No` (ou équivalente). Seule cette colonne est utilisée, les autres sont ignorées. +- Recommandation : vérifier que les numéros sont présents dans OpenDCIM afin de limiter les créations minimales. + +--- + +## Workflow Steps (EN) +1. **Upload** the Excel file; headers come from row 1. +2. **Select** the serial-number column; deduped values feed the analysis. +3. **Categorize** devices: + - Category 1: unknown serials (not in DB). + - Category 2: devices still installed (Cabinet > 0). + - Category 3: devices in Storage Room (Cabinet = -1). + - Category 4: already disposed (Cabinet = 0). +4. **Actions**: ignore/create minimal entries, move to storage, confirm disposal, etc. +5. **Finalize**: choose disposition method/date, run `Device::Dispose()` for confirmed devices and log each action. +6. **Log & Rollback**: download CSV log; rollback restores cabinet/position and removes fac_DispositionMembership updates. +7. **Reset**: reloading the page resets the wizard for a new file. + +## Étapes du workflow (FR) +1. **Importer** le fichier Excel ; les en-têtes proviennent de la première ligne. +2. **Sélectionner** la colonne contenant les numéros de série ; les valeurs sont dédupliquées. +3. **Catégoriser** les équipements : + - Catégorie 1 : numéros inconnus (absents de la base). + - Catégorie 2 : équipements encore installés (Cabinet > 0). + - Catégorie 3 : équipements présents en Storage Room (Cabinet = -1). + - Catégorie 4 : équipements déjà sortis (Cabinet = 0). +4. **Actions** : ignorer/créer des entrées minimales, déplacer en Storage Room, confirmer la méthode de sortie, etc. +5. **Finaliser** : choisir méthode/date, lancer `Device::Dispose()` pour chaque équipement confirmé avec journalisation. +6. **Journal & rollback** : export CSV, bouton d’annulation restaurent armoires/positions et nettoient fac_DispositionMembership. +7. **Réinitialisation** : actualiser la page (GET) redémarre l’assistant pour un nouveau fichier. + +--- + +## Best Practices (EN) +- Validate serial coverage before import; use minimal entries only when required. +- Run a full DB backup before starting (warning displayed in UI). +- Download the log after each session to keep an auditable record. + +## Bonnes pratiques (FR) +- Vérifier la présence des numéros avant import pour éviter les créations minimales. +- Faire une sauvegarde complète avant le traitement automatisé (avertissement sur la page). +- Télécharger le journal après chaque session pour tracer les opérations. From 94c34150b3b1840ca37d542e534b9a631f18ed46 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:13:42 +0100 Subject: [PATCH 5/6] Document disposal processing workflow --- DisposalProcessing.php | 180 ++++++++++++++++++++++++++- README_DisposalProcessing.md | 66 +++++----- locale/en_US/LC_MESSAGES/openDCIM.po | 88 +++++++++++++ locale/fr_FR/LC_MESSAGES/openDCIM.po | 88 +++++++++++++ 4 files changed, 384 insertions(+), 38 deletions(-) diff --git a/DisposalProcessing.php b/DisposalProcessing.php index defa8915c..1ea3386f0 100644 --- a/DisposalProcessing.php +++ b/DisposalProcessing.php @@ -37,7 +37,8 @@ function disposalDefaultState(){ 'log'=>array(), 'disposition_id'=>0, 'operation_date'=>date('Y-m-d'), - 'completed'=>false + 'completed'=>false, + 'cleanup_selected'=>array() ); } @@ -185,6 +186,77 @@ function disposalPerformRollback(&$state,&$messages,&$errors){ $messages[]=__("The rollback completed successfully."); } + function disposalAvailableCleanupOptions(){ + return array( + 'power'=>array('label'=>__("Clean power connections (fac_PowerPorts)")), + 'network'=>array('label'=>__("Clean network connections (fac_Ports)")), + 'projects'=>array('label'=>__("Remove project assignments (fac_ProjectMembership)")), + 'tags'=>array('label'=>__("Remove tags (fac_DeviceTags)")), + 'custom'=>array('label'=>__("Remove custom attributes (fac_DeviceCustomAttribute)")), + 'vm'=>array('label'=>__("Remove VM Inventory entries (fac_VMInventory)")), + 'logs'=>array('label'=>__("Clean logs (fac_GenericLog)"),'note'=>__("Not recommended: removing logs erases historical traceability.")), + 'rack'=>array('label'=>__("Clean rack request history (fac_RackRequest)")) + ); + } + + function disposalCleanupDevice($device,$options,&$state,$record){ + global $dbh; + + if(empty($options) || !is_array($options)){ + return; + } + + foreach($options as $option){ + switch($option){ + case 'power': + $st=$dbh->prepare("DELETE FROM fac_PowerPorts WHERE DeviceID=:DeviceID"); + $st->execute(array(":DeviceID"=>$device->DeviceID)); + disposalAppendLog($state,$record,__("Power connections cleaned"),'cleanup_power',array('cleanup_option'=>'power')); + break; + case 'network': + $st=$dbh->prepare("DELETE FROM fac_Ports WHERE DeviceID=:DeviceID"); + $st->execute(array(":DeviceID"=>$device->DeviceID)); + disposalAppendLog($state,$record,__("Network connections cleaned"),'cleanup_network',array('cleanup_option'=>'network')); + break; + case 'projects': + $st=$dbh->prepare("DELETE FROM fac_ProjectMembership WHERE MemberType='Device' AND MemberID=:DeviceID"); + $st->execute(array(":DeviceID"=>$device->DeviceID)); + disposalAppendLog($state,$record,__("Project assignments removed"),'cleanup_projects',array('cleanup_option'=>'projects')); + break; + case 'tags': + $st=$dbh->prepare("DELETE FROM fac_DeviceTags WHERE DeviceID=:DeviceID"); + $st->execute(array(":DeviceID"=>$device->DeviceID)); + disposalAppendLog($state,$record,__("Device tags removed"),'cleanup_tags',array('cleanup_option'=>'tags')); + break; + case 'custom': + $st=$dbh->prepare("DELETE FROM fac_DeviceCustomValue WHERE DeviceID=:DeviceID"); + $st->execute(array(":DeviceID"=>$device->DeviceID)); + disposalAppendLog($state,$record,__("Custom attributes removed"),'cleanup_custom',array('cleanup_option'=>'custom')); + break; + case 'vm': + $st=$dbh->prepare("DELETE FROM fac_VMInventory WHERE DeviceID=:DeviceID"); + $st->execute(array(":DeviceID"=>$device->DeviceID)); + disposalAppendLog($state,$record,__("VM inventory entries removed"),'cleanup_vm',array('cleanup_option'=>'vm')); + break; + case 'logs': + $st=$dbh->prepare("DELETE FROM fac_GenericLog WHERE Class='Device' AND ObjectID=:DeviceID"); + $st->execute(array(":DeviceID"=>$device->DeviceID)); + disposalAppendLog($state,$record,__("Device logs cleaned"),'cleanup_logs',array('cleanup_option'=>'logs')); + break; + case 'rack': + if($device->SerialNo!=''){ + // RackRequest table tracks SerialNo instead of DeviceID + $st=$dbh->prepare("DELETE FROM fac_RackRequest WHERE SerialNo=:SerialNo"); + $st->execute(array(":SerialNo"=>$device->SerialNo)); + } + disposalAppendLog($state,$record,__("Rack request history cleaned"),'cleanup_rack',array('cleanup_option'=>'rack')); + break; + default: + break; + } + } + } + $messages=array(); $errors=array(); $state=disposalLoadState(); @@ -401,6 +473,17 @@ function disposalPerformRollback(&$state,&$messages,&$errors){ if($state['actions']['final_processed']){ break; } + $cleanupSelected=array(); + if(isset($_POST['cleanup_options']) && is_array($_POST['cleanup_options'])){ + foreach($_POST['cleanup_options'] as $optionKey){ + $filtered=preg_replace('/[^a-z_]/','',$optionKey); + if($filtered!=''){ + $cleanupSelected[$filtered]=$filtered; + } + } + } + $cleanupSelected=array_values($cleanupSelected); + $state['cleanup_selected']=$cleanupSelected; if(!$state['actions']['storage_confirmed']){ $errors[]=__("Please confirm the Storage Room devices first."); break; @@ -426,6 +509,9 @@ function disposalPerformRollback(&$state,&$messages,&$errors){ $dev=new Device($deviceID); if($dev->GetDevice()){ $prevState=array('cabinet'=>$dev->Cabinet,'position'=>$dev->Position,'status'=>$dev->Status); + if(!empty($cleanupSelected)){ + disposalCleanupDevice($dev,$cleanupSelected,$state,$record); + } $dev->Dispose($dispID); $st=$dbh->prepare("UPDATE fac_DispositionMembership SET DispositionDate=:DispositionDate WHERE DeviceID=:DeviceID"); $st->execute(array(":DispositionDate"=>$operationDate,":DeviceID"=>$deviceID)); @@ -484,6 +570,23 @@ function disposalPerformRollback(&$state,&$messages,&$errors){ .dp-category-notstorage{border-color:#1e90ff;background:#f0f8ff;} .dp-category-storage{border-color:#2e8b57;background:#f5fff7;} .dp-category-disposed{border-color:#777;background:#f2f2f2;} + .dp-cleanup-warning{font-size:.9em;background:#fff8e1;border:1px solid #f0c36d;color:#8a6d3b;margin-bottom:10px;padding:8px;} + .dp-cleanup-block{border:1px dashed #d0d0d0;background:#fff;padding:10px;margin-bottom:15px;} + .dp-cleanup-title{font-weight:bold;margin-bottom:6px;} + .dp-cleanup-options{display:flex;flex-direction:column;gap:6px;} + .dp-cleanup-option{display:flex;flex-direction:column;font-size:.95em;} + .dp-cleanup-line{display:flex;align-items:center;gap:6px;} + .dp-cleanup-option input[type="checkbox"]{margin-right:6px;} + .dp-cleanup-option>span{margin-left:0;} + .dp-cleanup-note{font-size:.8em;color:#a94442;margin-left:24px;} + .dp-form-row{margin-bottom:12px;} + .dp-form-row label{display:block;font-weight:bold;margin-bottom:4px;} + .dp-form-row input[type="text"], .dp-form-row select{max-width:360px;width:100%;} + .dp-progress{margin-top:10px;display:none;} + .dp-progress-bar{height:18px;background:#eee;border-radius:4px;overflow:hidden;} + .dp-progress-fill{display:block;height:100%;width:0;background:#4caf50;transition:width .4s ease;} + .dp-progress-meta{display:flex;justify-content:space-between;font-size:.85em;margin-top:4px;} + .dp-progress-status{color:#2e8b57;} @@ -681,9 +784,26 @@ function disposalPerformRollback(&$state,&$messages,&$errors){

-
+ -
+
+
+
+
+ $optionMeta){ + $checked=(in_array($optionKey,$state['cleanup_selected']))?' checked':''; + echo ''; + } + ?> +
+
+
-
+
-
- + +
+
+ +
+
+ % + +
+
+
+
@@ -754,6 +884,44 @@ function disposalPerformRollback(&$state,&$messages,&$errors){ diff --git a/README_DisposalProcessing.md b/README_DisposalProcessing.md index dc91a863e..455c4a7bf 100644 --- a/README_DisposalProcessing.md +++ b/README_DisposalProcessing.md @@ -1,4 +1,4 @@ -# Disposal Processing Workflow +# Disposal Processing Workflow ## Overview (English) The `DisposalProcessing.php` page, available under **Bulk Importer > Process Scrap**, guides Site Administrators through an automated disposal workflow using the logistics Excel file. It imports serial numbers, classifies devices into four categories, offers per-category actions, and records every step for auditing/rollback. @@ -9,53 +9,55 @@ La page `DisposalProcessing.php`, accessible via **Bulk Importer > Process Scrap --- ## Expected File (EN) -- **Format**: Excel XLSX/XLS (one per run). -- **Source**: Logistics spreadsheet listing devices slated for disposal. -- **Pivot Column**: `Serial/No` (or similar). Only this column is used—others are ignored. +- **Format**: Excel XLSX/XLS (one per run). +- **Source**: Logistics spreadsheet listing devices slated for disposal. +- **Pivot Column**: `Serial/No` (or similar). Only this column is used—others are ignored. - Recommendation: ensure serials exist in OpenDCIM to minimize minimal-entry creation. ## Fichier attendu (FR) -- **Format** : Excel XLSX/XLS (un par session). -- **Origine** : Fichier logistique listant les équipements destinés à la destruction/recyclage. -- **Colonne pivot** : `Serial/No` (ou équivalente). Seule cette colonne est utilisée, les autres sont ignorées. +- **Format** : Excel XLSX/XLS (un par session). +- **Origine** : Fichier logistique listant les équipements destinés à la destruction/recyclage. +- **Colonne pivot** : `Serial/No` (ou équivalente). Seule cette colonne est utilisée, les autres sont ignorées. - Recommandation : vérifier que les numéros sont présents dans OpenDCIM afin de limiter les créations minimales. --- ## Workflow Steps (EN) -1. **Upload** the Excel file; headers come from row 1. -2. **Select** the serial-number column; deduped values feed the analysis. +1. **Upload** the Excel file; headers come from row 1. +2. **Select** the serial-number column; deduped values feed the analysis. 3. **Categorize** devices: - - Category 1: unknown serials (not in DB). - - Category 2: devices still installed (Cabinet > 0). - - Category 3: devices in Storage Room (Cabinet = -1). - - Category 4: already disposed (Cabinet = 0). -4. **Actions**: ignore/create minimal entries, move to storage, confirm disposal, etc. -5. **Finalize**: choose disposition method/date, run `Device::Dispose()` for confirmed devices and log each action. -6. **Log & Rollback**: download CSV log; rollback restores cabinet/position and removes fac_DispositionMembership updates. -7. **Reset**: reloading the page resets the wizard for a new file. + - Category 1: unknown serials (not in DB). + - Category 2: devices still installed (Cabinet > 0). + - Category 3: devices in Storage Room (Cabinet = -1). + - Category 4: already disposed (Cabinet = 0). +4. **Optional cleanups**: choose power/network/project/tag/custom/VM/log/rack cleanups to run before disposal. +5. **Actions**: ignore/create minimal entries, move to storage, confirm disposal, etc. +6. **Finalize**: choose disposition method/date, run `Device::Dispose()` for confirmed devices and log each action. +7. **Log & Rollback**: download CSV log; rollback restores cabinet/position and removes fac_DispositionMembership updates. +8. **Reset**: reloading the page resets the wizard for a new file. ## Étapes du workflow (FR) -1. **Importer** le fichier Excel ; les en-têtes proviennent de la première ligne. -2. **Sélectionner** la colonne contenant les numéros de série ; les valeurs sont dédupliquées. +1. **Importer** le fichier Excel ; les en-têtes proviennent de la première ligne. +2. **Sélectionner** la colonne contenant les numéros de série ; les valeurs sont dédupliquées. 3. **Catégoriser** les équipements : - - Catégorie 1 : numéros inconnus (absents de la base). - - Catégorie 2 : équipements encore installés (Cabinet > 0). - - Catégorie 3 : équipements présents en Storage Room (Cabinet = -1). - - Catégorie 4 : équipements déjà sortis (Cabinet = 0). -4. **Actions** : ignorer/créer des entrées minimales, déplacer en Storage Room, confirmer la méthode de sortie, etc. -5. **Finaliser** : choisir méthode/date, lancer `Device::Dispose()` pour chaque équipement confirmé avec journalisation. -6. **Journal & rollback** : export CSV, bouton d’annulation restaurent armoires/positions et nettoient fac_DispositionMembership. -7. **Réinitialisation** : actualiser la page (GET) redémarre l’assistant pour un nouveau fichier. + - Catégorie 1 : numéros inconnus (absents de la base). + - Catégorie 2 : équipements encore installés (Cabinet > 0). + - Catégorie 3 : équipements présents en Storage Room (Cabinet = -1). + - Catégorie 4 : équipements déjà sortis (Cabinet = 0). +4. **Nettoyages optionnels** : choisir les actions (énergie/réseau/projets/tags/attributs/VM/journaux/historique rack) à exécuter avant la destruction. +5. **Actions** : ignorer/créer des entrées minimales, déplacer en Storage Room, confirmer la méthode de sortie, etc. +6. **Traitement final** : choisir méthode/date, lancer `Device::Dispose()` pour chaque équipement confirmé avec journalisation. +7. **Journal & rollback** : export CSV, bouton d’annulation restaurent armoires/positions et nettoient fac_DispositionMembership. +8. **Réinitialisation** : actualiser la page (GET) redémarre l’assistant pour un nouveau fichier. --- ## Best Practices (EN) -- Validate serial coverage before import; use minimal entries only when required. -- Run a full DB backup before starting (warning displayed in UI). +- Validate serial coverage before import; use minimal entries only when required. +- Run a full DB backup before starting (warning displayed in UI). - Download the log after each session to keep an auditable record. ## Bonnes pratiques (FR) -- Vérifier la présence des numéros avant import pour éviter les créations minimales. -- Faire une sauvegarde complète avant le traitement automatisé (avertissement sur la page). -- Télécharger le journal après chaque session pour tracer les opérations. +- Vérifier la présence des numéros avant import pour éviter les créations minimales. +- Faire une sauvegarde complète avant le traitement automatisé (avertissement sur la page). +- Télécharger le journal après chaque session pour conserver une trace exploitable. diff --git a/locale/en_US/LC_MESSAGES/openDCIM.po b/locale/en_US/LC_MESSAGES/openDCIM.po index 957a1a81c..f3a487707 100644 --- a/locale/en_US/LC_MESSAGES/openDCIM.po +++ b/locale/en_US/LC_MESSAGES/openDCIM.po @@ -6421,3 +6421,91 @@ msgstr "The rollback completed successfully." msgid "Please select the column that contains the serial numbers." msgstr "Please select the column that contains the serial numbers." +#: DisposalProcessing.php +msgid "Clean power connections (fac_PowerPorts)" +msgstr "Clean power connections (fac_PowerPorts)" + +#: DisposalProcessing.php +msgid "Clean network connections (fac_Ports)" +msgstr "Clean network connections (fac_Ports)" + +#: DisposalProcessing.php +msgid "Remove project assignments (fac_ProjectMembership)" +msgstr "Remove project assignments (fac_ProjectMembership)" + +#: DisposalProcessing.php +msgid "Remove tags (fac_DeviceTags)" +msgstr "Remove tags (fac_DeviceTags)" + +#: DisposalProcessing.php +msgid "Remove custom attributes (fac_DeviceCustomAttribute)" +msgstr "Remove custom attributes (fac_DeviceCustomAttribute)" + +#: DisposalProcessing.php +msgid "Remove VM Inventory entries (fac_VMInventory)" +msgstr "Remove VM Inventory entries (fac_VMInventory)" + +#: DisposalProcessing.php +msgid "Clean logs (fac_GenericLog)" +msgstr "Clean logs (fac_GenericLog)" + +#: DisposalProcessing.php +msgid "Not recommended: removing logs erases historical traceability." +msgstr "Not recommended: removing logs erases historical traceability." + +#: DisposalProcessing.php +msgid "Clean rack request history (fac_RackRequest)" +msgstr "Clean rack request history (fac_RackRequest)" + +#: DisposalProcessing.php +msgid "Power connections cleaned" +msgstr "Power connections cleaned" + +#: DisposalProcessing.php +msgid "Network connections cleaned" +msgstr "Network connections cleaned" + +#: DisposalProcessing.php +msgid "Project assignments removed" +msgstr "Project assignments removed" + +#: DisposalProcessing.php +msgid "Device tags removed" +msgstr "Device tags removed" + +#: DisposalProcessing.php +msgid "Custom attributes removed" +msgstr "Custom attributes removed" + +#: DisposalProcessing.php +msgid "VM inventory entries removed" +msgstr "VM inventory entries removed" + +#: DisposalProcessing.php +msgid "Device logs cleaned" +msgstr "Device logs cleaned" + +#: DisposalProcessing.php +msgid "Rack request history cleaned" +msgstr "Rack request history cleaned" + +#: DisposalProcessing.php +msgid "Optional cleanups help remove related records before final processing." +msgstr "Optional cleanups help remove related records before final processing." + +#: DisposalProcessing.php +msgid "Optional Cleanup Operations" +msgstr "Optional Cleanup Operations" + +#: DisposalProcessing.php +msgid "Processing completed successfully." +msgstr "Processing completed successfully." + +#: DisposalProcessing.php +msgid "Processing in progress..." +msgstr "Processing in progress..." + +#: DisposalProcessing.php +msgid "Initializing" +msgstr "Initializing" + diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index 65a1dfa21..0590927b3 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6549,3 +6549,91 @@ msgstr "Le retour arrière a été effectué avec succès." msgid "Please select the column that contains the serial numbers." msgstr "Veuillez sélectionner la colonne contenant les numéros de série." +#: DisposalProcessing.php +msgid "Clean power connections (fac_PowerPorts)" +msgstr "Nettoyer les connexions électriques (fac_PowerPorts)" + +#: DisposalProcessing.php +msgid "Clean network connections (fac_Ports)" +msgstr "Nettoyer les connexions réseau (fac_Ports)" + +#: DisposalProcessing.php +msgid "Remove project assignments (fac_ProjectMembership)" +msgstr "Supprimer les affectations de projet (fac_ProjectMembership)" + +#: DisposalProcessing.php +msgid "Remove tags (fac_DeviceTags)" +msgstr "Supprimer les tags (fac_DeviceTags)" + +#: DisposalProcessing.php +msgid "Remove custom attributes (fac_DeviceCustomAttribute)" +msgstr "Supprimer les attributs personnalisés (fac_DeviceCustomAttribute)" + +#: DisposalProcessing.php +msgid "Remove VM Inventory entries (fac_VMInventory)" +msgstr "Supprimer les entrées d'inventaire VM (fac_VMInventory)" + +#: DisposalProcessing.php +msgid "Clean logs (fac_GenericLog)" +msgstr "Nettoyer les journaux (fac_GenericLog)" + +#: DisposalProcessing.php +msgid "Not recommended: removing logs erases historical traceability." +msgstr "Non recommandé : supprimer les journaux efface la traçabilité historique." + +#: DisposalProcessing.php +msgid "Clean rack request history (fac_RackRequest)" +msgstr "Nettoyer l'historique des demandes de rack (fac_RackRequest)" + +#: DisposalProcessing.php +msgid "Power connections cleaned" +msgstr "Connexions électriques nettoyées" + +#: DisposalProcessing.php +msgid "Network connections cleaned" +msgstr "Connexions réseau nettoyées" + +#: DisposalProcessing.php +msgid "Project assignments removed" +msgstr "Affectations de projet supprimées" + +#: DisposalProcessing.php +msgid "Device tags removed" +msgstr "Tags de l'équipement supprimés" + +#: DisposalProcessing.php +msgid "Custom attributes removed" +msgstr "Attributs personnalisés supprimés" + +#: DisposalProcessing.php +msgid "VM inventory entries removed" +msgstr "Entrées d'inventaire VM supprimées" + +#: DisposalProcessing.php +msgid "Device logs cleaned" +msgstr "Journaux de l'équipement nettoyés" + +#: DisposalProcessing.php +msgid "Rack request history cleaned" +msgstr "Historique des demandes de rack nettoyé" + +#: DisposalProcessing.php +msgid "Optional cleanups help remove related records before final processing." +msgstr "Les nettoyages optionnels permettent de supprimer les données liées avant le traitement final." + +#: DisposalProcessing.php +msgid "Optional Cleanup Operations" +msgstr "Opérations de nettoyage optionnelles" + +#: DisposalProcessing.php +msgid "Processing completed successfully." +msgstr "Traitement achevé avec succès." + +#: DisposalProcessing.php +msgid "Processing in progress..." +msgstr "Traitement en cours..." + +#: DisposalProcessing.php +msgid "Initializing" +msgstr "Initialisation" + From ee94228ceaf6cb04a405a9bc0b7daa6c43dcd0e7 Mon Sep 17 00:00:00 2001 From: Alexandre <146840804+alex001x@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:09:01 +0100 Subject: [PATCH 6/6] Document disposal processing workflow --- DisposalProcessing.php | 239 ++++++++++++++++++++++----- README_DisposalProcessing.md | 8 +- locale/en_US/LC_MESSAGES/openDCIM.po | 40 +++++ locale/fr_FR/LC_MESSAGES/openDCIM.po | 40 +++++ 4 files changed, 279 insertions(+), 48 deletions(-) diff --git a/DisposalProcessing.php b/DisposalProcessing.php index 1ea3386f0..f2a846aab 100644 --- a/DisposalProcessing.php +++ b/DisposalProcessing.php @@ -17,8 +17,6 @@ function disposalDefaultState(){ return array( 'stage'=>'idle', 'file'=>'', - 'headers'=>array(), - 'selected_column'=>-1, 'serials'=>array(), 'categories'=>array( 'unknown'=>array(), @@ -30,7 +28,6 @@ function disposalDefaultState(){ 'unknown_ignored'=>false, 'unknown_created'=>false, 'not_storage_moved'=>false, - 'storage_confirmed'=>false, 'final_processed'=>false, 'rollback_done'=>false ), @@ -38,7 +35,10 @@ function disposalDefaultState(){ 'disposition_id'=>0, 'operation_date'=>date('Y-m-d'), 'completed'=>false, - 'cleanup_selected'=>array() + 'cleanup_selected'=>array(), + 'sheets'=>array(), + 'selected_sheets'=>array(), + 'sheet_columns'=>array() ); } @@ -257,6 +257,61 @@ function disposalCleanupDevice($device,$options,&$state,$record){ } } + function disposalNormalizeHeader($value){ + $value=trim(mb_strtolower($value ?? '')); + if(function_exists('iconv')){ + $converted=@iconv('UTF-8','ASCII//TRANSLIT//IGNORE',$value); + if($converted!==false){ + $value=$converted; + } + } + $value=preg_replace('/[^a-z0-9]+/','',$value); + return $value; + } + + function disposalGuessSerialColumn($headers){ + $references=array('sn','serialno','serialnumber','serialnum','serialnumber','numerodeserie','numerodeserie','ndeserie'); + foreach($headers as $index=>$header){ + $normalized=disposalNormalizeHeader($header); + if($normalized=='' ){ + continue; + } + if(in_array($normalized,$references)){ + return $index; + } + } + return -1; + } + + function disposalDetectSheetHeader($sheet){ + $highestColumn=\PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($sheet->getHighestColumn()); + $highestRow=min(20,$sheet->getHighestRow()); + for($row=1;$row<=$highestRow;$row++){ + $headers=array(); + $nonEmpty=0; + $textCount=0; + for($col=1;$col<=$highestColumn;$col++){ + $cell=$sheet->getCellByColumnAndRow($col,$row); + $value=''; + if(!$cell->isInMergeRange()){ + $value=trim((string)$cell->getCalculatedValue()); + } + if($value!==''){ + $nonEmpty++; + if(preg_match('/[A-Za-z]/u',$value)){ + $textCount++; + } + } + $headers[]=$value; + } + if($nonEmpty>=2 && $textCount>=1){ + $snIndex=disposalGuessSerialColumn($headers); + return array('row'=>$row,'headers'=>$headers,'sn_column'=>$snIndex); + } + } + return null; + } + $messages=array(); $errors=array(); $state=disposalLoadState(); @@ -303,10 +358,24 @@ function disposalCleanupDevice($device,$options,&$state,$record){ $errors[]=__("The uploaded file could not be read."); break; } - $sheet=$spreadsheet->getSheet(0); - $highestColumn=$sheet->getHighestColumn(); - $headerRow=$sheet->rangeToArray('A1:'.$highestColumn.'1'); - $headers=(isset($headerRow[0]) && is_array($headerRow[0]))?$headerRow[0]:array(); + $sheetDetails=array(); + for($idx=0;$idx<$spreadsheet->getSheetCount();$idx++){ + $currentSheet=$spreadsheet->getSheet($idx); + $headerInfo=disposalDetectSheetHeader($currentSheet); + if($headerInfo){ + $sheetDetails[$idx]=array( + 'name'=>$currentSheet->getTitle(), + 'header_row'=>$headerInfo['row'], + 'headers'=>$headerInfo['headers'], + 'detected_column'=>$headerInfo['sn_column'] + ); + } + } + if(empty($sheetDetails)){ + $errors[]=__("No valid worksheets were detected in the uploaded file."); + @unlink($target); + break; + } $tmp=tempnam(sys_get_temp_dir(),'dp_'); @unlink($tmp); $target=$tmp.'.xlsx'; @@ -315,20 +384,69 @@ function disposalCleanupDevice($device,$options,&$state,$record){ break; } $state['file']=$target; - $state['headers']=$headers; + $state['sheets']=$sheetDetails; + $state['selected_sheets']=array_keys($sheetDetails); + $state['sheet_columns']=array(); + $state['stage']='select_sheets'; + $messages[]=__("File uploaded successfully. Please select the worksheets to process."); + break; + case 'select-sheets': + if(empty($_POST['sheets']) || !is_array($_POST['sheets'])){ + $errors[]=__("Please select at least one worksheet."); + break; + } + $selected=array(); + foreach($_POST['sheets'] as $sheetKey){ + $sheetIndex=intval($sheetKey); + if(isset($state['sheets'][$sheetIndex])){ + $selected[]=$sheetIndex; + } + } + if(empty($selected)){ + $errors[]=__("Please select at least one worksheet."); + break; + } + $state['selected_sheets']=$selected; + $state['sheet_columns']=array(); + foreach($selected as $sheetIndex){ + $detected=$state['sheets'][$sheetIndex]['detected_column']; + if($detected>=0){ + $state['sheet_columns'][$sheetIndex]=$detected; + } + } $state['stage']='select_column'; - $messages[]=__("File uploaded successfully. Please select the column that contains the serial numbers."); break; - case 'select-column': + case 'set-columns': if($state['file']=='' || !file_exists($state['file'])){ $errors[]=__("The uploaded file could not be read."); break; } - $columnIndex=isset($_POST['selected_column'])?intval($_POST['selected_column']):-1; - if($columnIndex<0 || $columnIndex>=count($state['headers'])){ - $errors[]=__("Please select a valid column."); + if(empty($state['selected_sheets'])){ + $errors[]=__("Please select at least one worksheet."); break; } + $requestedColumns=isset($_POST['sheet_columns']) && is_array($_POST['sheet_columns'])?$_POST['sheet_columns']:array(); + $sheetColumns=array(); + $columnError=false; + foreach($state['selected_sheets'] as $sheetIndex){ + if(!isset($requestedColumns[$sheetIndex]) || $requestedColumns[$sheetIndex]===''){ + $errors[]=sprintf(__("Please select a serial number column for sheet %s."),$state['sheets'][$sheetIndex]['name']); + $columnError=true; + break; + } + $colIndex=intval($requestedColumns[$sheetIndex]); + $headers=$state['sheets'][$sheetIndex]['headers']; + if($colIndex<0 || $colIndex>=count($headers) || trim($headers[$colIndex])===''){ + $errors[]=sprintf(__("Invalid column selection for sheet %s."),$state['sheets'][$sheetIndex]['name']); + $columnError=true; + break; + } + $sheetColumns[$sheetIndex]=$colIndex; + } + if($columnError){ + break; + } + $state['sheet_columns']=$sheetColumns; try{ $inputType=\PhpOffice\PhpSpreadsheet\IOFactory::identify($state['file']); $reader=\PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputType); @@ -337,14 +455,19 @@ function disposalCleanupDevice($device,$options,&$state,$record){ $errors[]=__("The uploaded file could not be read."); break; } - $sheet=$spreadsheet->getSheet(0); - $highestRow=$sheet->getHighestRow(); $serials=array(); - for($row=2;$row<=$highestRow;$row++){ - $value=$sheet->getCellByColumnAndRow($columnIndex+1,$row)->getCalculatedValue(); - $serial=trim((string)$value); - if($serial!==''){ - $serials[$serial]=$serial; + foreach($state['selected_sheets'] as $sheetIndex){ + $sheet=$spreadsheet->getSheet($sheetIndex); + $headerRow=$state['sheets'][$sheetIndex]['header_row']; + $columnNumber=$sheetColumns[$sheetIndex]+1; + $highestRow=$sheet->getHighestRow(); + for($row=$headerRow+1;$row<=$highestRow;$row++){ + $cell=$sheet->getCellByColumnAndRow($columnNumber,$row); + $value=$cell->getCalculatedValue(); + $serial=trim((string)$value); + if($serial!==''){ + $serials[$serial]=$serial; + } } } $serials=array_values($serials); @@ -379,7 +502,6 @@ function disposalCleanupDevice($device,$options,&$state,$record){ } $state['serials']=$serials; $state['categories']=$categories; - $state['selected_column']=$columnIndex; $state['stage']='ready'; $messages[]=__("Serial numbers ready for processing."); break; @@ -465,10 +587,6 @@ function disposalCleanupDevice($device,$options,&$state,$record){ $state['actions']['not_storage_moved']=true; $messages[]=__("Devices moved to the Storage Room."); break; - case 'confirm-storage': - $state['actions']['storage_confirmed']=true; - $messages[]=__("Storage Room devices confirmed. You may proceed with the final processing."); - break; case 'start-processing': if($state['actions']['final_processed']){ break; @@ -484,10 +602,6 @@ function disposalCleanupDevice($device,$options,&$state,$record){ } $cleanupSelected=array_values($cleanupSelected); $state['cleanup_selected']=$cleanupSelected; - if(!$state['actions']['storage_confirmed']){ - $errors[]=__("Please confirm the Storage Room devices first."); - break; - } if(empty($state['categories']['storage_ready'])){ $errors[]=__("No devices are ready in the Storage Room."); break; @@ -587,6 +701,7 @@ function disposalCleanupDevice($device,$options,&$state,$record){ .dp-progress-fill{display:block;height:100%;width:0;background:#4caf50;transition:width .4s ease;} .dp-progress-meta{display:flex;justify-content:space-between;font-size:.85em;margin-top:4px;} .dp-progress-status{color:#2e8b57;} + .dp-sn-warning{font-size:.85em;color:#a94442;margin-top:4px;} @@ -620,17 +735,55 @@ function disposalCleanupDevice($device,$options,&$state,$record){
+ +
+ +
+
+
+ $sheetInfo){ + $checked=(in_array($sheetIndex,$state['selected_sheets']))?' checked':''; + ?> + + +
+
+
+
+
+ + +
- -
- - + +
+ + + +
+
+
@@ -741,14 +894,7 @@ function disposalCleanupDevice($device,$options,&$state,$record){ } ?>
- - - - -
- - -
+



@@ -839,6 +985,11 @@ function disposalCleanupDevice($device,$options,&$state,$record){

+
+
+ +
+
diff --git a/README_DisposalProcessing.md b/README_DisposalProcessing.md index 455c4a7bf..daaebd39f 100644 --- a/README_DisposalProcessing.md +++ b/README_DisposalProcessing.md @@ -23,8 +23,8 @@ La page `DisposalProcessing.php`, accessible via **Bulk Importer > Process Scrap --- ## Workflow Steps (EN) -1. **Upload** the Excel file; headers come from row 1. -2. **Select** the serial-number column; deduped values feed the analysis. +1. **Upload** the Excel file; headers can be detected up to row 20. +2. **Select** the worksheet tabs to include and confirm/adjust the detected Serial Number column (auto-mapped to labels like “SN”, “Serial Number”, etc.). 3. **Categorize** devices: - Category 1: unknown serials (not in DB). - Category 2: devices still installed (Cabinet > 0). @@ -37,8 +37,8 @@ La page `DisposalProcessing.php`, accessible via **Bulk Importer > Process Scrap 8. **Reset**: reloading the page resets the wizard for a new file. ## Étapes du workflow (FR) -1. **Importer** le fichier Excel ; les en-têtes proviennent de la première ligne. -2. **Sélectionner** la colonne contenant les numéros de série ; les valeurs sont dédupliquées. +1. **Importer** le fichier Excel ; les en-têtes sont détectées jusqu’à la ligne 20. +2. **Sélectionner** les onglets à traiter et confirmer/ajuster la colonne de numéro de série détectée (SN, Serial Number, Numéro de série, etc.). 3. **Catégoriser** les équipements : - Catégorie 1 : numéros inconnus (absents de la base). - Catégorie 2 : équipements encore installés (Cabinet > 0). diff --git a/locale/en_US/LC_MESSAGES/openDCIM.po b/locale/en_US/LC_MESSAGES/openDCIM.po index f3a487707..56d870250 100644 --- a/locale/en_US/LC_MESSAGES/openDCIM.po +++ b/locale/en_US/LC_MESSAGES/openDCIM.po @@ -6421,6 +6421,46 @@ msgstr "The rollback completed successfully." msgid "Please select the column that contains the serial numbers." msgstr "Please select the column that contains the serial numbers." +#: DisposalProcessing.php +msgid "File uploaded successfully. Please select the worksheets to process." +msgstr "File uploaded successfully. Please select the worksheets to process." + +#: DisposalProcessing.php +msgid "No valid worksheets were detected in the uploaded file." +msgstr "No valid worksheets were detected in the uploaded file." + +#: DisposalProcessing.php +msgid "Please select at least one worksheet." +msgstr "Please select at least one worksheet." + +#: DisposalProcessing.php +msgid "Please select a serial number column for sheet %s." +msgstr "Please select a serial number column for sheet %s." + +#: DisposalProcessing.php +msgid "Invalid column selection for sheet %s." +msgstr "Invalid column selection for sheet %s." + +#: DisposalProcessing.php +msgid "Select worksheets to process" +msgstr "Select worksheets to process" + +#: DisposalProcessing.php +msgid "Sheet \"%s\"" +msgstr "Sheet \"%s\"" + +#: DisposalProcessing.php +msgid "Sheet \"%s\" serial column" +msgstr "Sheet \"%s\" serial column" + +#: DisposalProcessing.php +msgid "The serial number column could not be detected. Please select the correct column." +msgstr "The serial number column could not be detected. Please select the correct column." + +#: DisposalProcessing.php +msgid "Review these devices before running the final processing step." +msgstr "Review these devices before running the final processing step." + #: DisposalProcessing.php msgid "Clean power connections (fac_PowerPorts)" msgstr "Clean power connections (fac_PowerPorts)" diff --git a/locale/fr_FR/LC_MESSAGES/openDCIM.po b/locale/fr_FR/LC_MESSAGES/openDCIM.po index 0590927b3..9f94f7387 100644 --- a/locale/fr_FR/LC_MESSAGES/openDCIM.po +++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po @@ -6549,6 +6549,46 @@ msgstr "Le retour arrière a été effectué avec succès." msgid "Please select the column that contains the serial numbers." msgstr "Veuillez sélectionner la colonne contenant les numéros de série." +#: DisposalProcessing.php +msgid "File uploaded successfully. Please select the worksheets to process." +msgstr "Fichier importé avec succès. Veuillez sélectionner les feuilles de calcul à traiter." + +#: DisposalProcessing.php +msgid "No valid worksheets were detected in the uploaded file." +msgstr "Aucune feuille de calcul valide n'a été détectée dans le fichier importé." + +#: DisposalProcessing.php +msgid "Please select at least one worksheet." +msgstr "Veuillez sélectionner au moins une feuille de calcul." + +#: DisposalProcessing.php +msgid "Please select a serial number column for sheet %s." +msgstr "Veuillez sélectionner la colonne des numéros de série pour la feuille %s." + +#: DisposalProcessing.php +msgid "Invalid column selection for sheet %s." +msgstr "Sélection de colonne invalide pour la feuille %s." + +#: DisposalProcessing.php +msgid "Select worksheets to process" +msgstr "Sélectionnez les feuilles de calcul à traiter" + +#: DisposalProcessing.php +msgid "Sheet \"%s\"" +msgstr "Feuille \"%s\"" + +#: DisposalProcessing.php +msgid "Sheet \"%s\" serial column" +msgstr "Colonne de numéro de série pour la feuille \"%s\"" + +#: DisposalProcessing.php +msgid "The serial number column could not be detected. Please select the correct column." +msgstr "La colonne des numéros de série n'a pas pu être détectée. Veuillez sélectionner la colonne appropriée." + +#: DisposalProcessing.php +msgid "Review these devices before running the final processing step." +msgstr "Vérifiez ces équipements avant de lancer l'étape de traitement final." + #: DisposalProcessing.php msgid "Clean power connections (fac_PowerPorts)" msgstr "Nettoyer les connexions électriques (fac_PowerPorts)"