+
+ %s
+
+
+ +
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(); +?> + + +
+ + +