diff --git a/DisposalProcessing.php b/DisposalProcessing.php
new file mode 100644
index 000000000..f2a846aab
--- /dev/null
+++ b/DisposalProcessing.php
@@ -0,0 +1,1079 @@
+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'=>'',
+ '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,
+ 'final_processed'=>false,
+ 'rollback_done'=>false
+ ),
+ 'log'=>array(),
+ 'disposition_id'=>0,
+ 'operation_date'=>date('Y-m-d'),
+ 'completed'=>false,
+ 'cleanup_selected'=>array(),
+ 'sheets'=>array(),
+ 'selected_sheets'=>array(),
+ 'sheet_columns'=>array()
+ );
+ }
+
+ 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.");
+ }
+
+ 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;
+ }
+ }
+ }
+
+ 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();
+
+ 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;
+ }
+ $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';
+ if(!move_uploaded_file($_FILES['disposalfile']['tmp_name'],$target)){
+ $errors[]=__("Invalid file uploaded.");
+ break;
+ }
+ $state['file']=$target;
+ $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';
+ break;
+ case 'set-columns':
+ if($state['file']=='' || !file_exists($state['file'])){
+ $errors[]=__("The uploaded file could not be read.");
+ break;
+ }
+ 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);
+ $spreadsheet=$reader->load($state['file']);
+ }catch(Exception $e){
+ $errors[]=__("The uploaded file could not be read.");
+ break;
+ }
+ $serials=array();
+ 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);
+ 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['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 'start-processing':
+ 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(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);
+ 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));
+ 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();
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
',htmlspecialchars($msg));
+ }
+ foreach($messages as $msg){
+ printf('
%s
',htmlspecialchars($msg));
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+ '.__("No records found.").'
';
+ }else{
+ foreach($state['categories']['unknown'] as $entry){
+ printf('
',htmlspecialchars($entry['serial']));
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ '.__("No records found.").'
';
+ }else{
+ foreach($state['categories']['storage_ready'] as $record){
+ printf('
',
+ $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('
',
+ $record['deviceid'],
+ htmlspecialchars($record['label']),
+ htmlspecialchars($record['serial']),
+ htmlspecialchars($statusNote),
+ htmlspecialchars($record['location'])
+ );
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README_DisposalProcessing.md b/README_DisposalProcessing.md
new file mode 100644
index 000000000..daaebd39f
--- /dev/null
+++ b/README_DisposalProcessing.md
@@ -0,0 +1,63 @@
+# 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.
+
+## 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 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).
+ - 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 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).
+ - 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).
+- 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 conserver une trace exploitable.
diff --git a/classes/Device.class.php b/classes/Device.class.php
index 999d048ec..1913edb87 100644
--- a/classes/Device.class.php
+++ b/classes/Device.class.php
@@ -1545,6 +1545,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 360fc3acb..ea6b7a1a9 100644
--- a/locale/en_US/LC_MESSAGES/openDCIM.po
+++ b/locale/en_US/LC_MESSAGES/openDCIM.po
@@ -6231,3 +6231,416 @@ 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."
+
+#: 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)"
+
+#: 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 4b1d3370d..3c761ff9d 100644
--- a/locale/fr_FR/LC_MESSAGES/openDCIM.po
+++ b/locale/fr_FR/LC_MESSAGES/openDCIM.po
@@ -6359,3 +6359,416 @@ 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."
+
+#: 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)"
+
+#: 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"
+
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").'';