3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/app/Importer/AssetHistoryImporter.php
2026-07-31 04:16:03 +01:00

261 lines
8.8 KiB
PHP

<?php
namespace App\Importer;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Setting;
use App\Models\User;
use Carbon\Carbon;
class AssetHistoryImporter extends Importer
{
protected $matchUsername = false;
protected $matchEmail = false;
protected $matchFirstnameLastname = false;
protected $matchFlastname = false;
protected $matchFirstname = false;
// Base defaultFieldMap doesn't know about the two date columns this
// importer uses, and its `full_name => "full name"` mapping doesn't
// match the historical CSV template's bare "Name" column. Merge these
// in every time setFieldMappings runs (which ItemImportRequest does
// even for callers that don't pass column-mappings, wiping constructor-
// level overrides).
private array $historyFieldMapExtras = [
'checkout_date' => 'checkout date',
'checkin_date' => 'checkin date',
'full_name' => 'name',
];
public function setFieldMappings($fields)
{
parent::setFieldMappings($fields);
$this->fieldMap = array_merge($this->fieldMap, $this->historyFieldMapExtras);
return $this;
}
public function setMatchUsername(bool $flag): self
{
$this->matchUsername = $flag;
return $this;
}
public function setMatchEmail(bool $flag): self
{
$this->matchEmail = $flag;
return $this;
}
public function setMatchFirstnameLastname(bool $flag): self
{
$this->matchFirstnameLastname = $flag;
return $this;
}
public function setMatchFlastname(bool $flag): self
{
$this->matchFlastname = $flag;
return $this;
}
public function setMatchFirstname(bool $flag): self
{
$this->matchFirstname = $flag;
return $this;
}
protected function handle($row)
{
$asset_tag = $this->findCsvMatch($row, 'asset_tag');
$name = $this->findCsvMatch($row, 'full_name');
$checkout_date_raw = $this->findCsvMatch($row, 'checkout_date');
$checkin_date_raw = $this->findCsvMatch($row, 'checkin_date');
// Was a checkin column present in the CSV at all? If not, the
// original importer assumes every row represents an already-
// checked-in asset (no assignment mutation, checkin actionlog
// stamped at import time).
$checkinHeaderPresent = array_key_exists($this->lookupCustomKey('checkin_date'), $row);
if (empty($asset_tag)) {
$this->log('Row is missing an asset tag - skipping');
$this->recordSkipped();
$this->addRowError(
trans('admin/hardware/message.import.history.missing_asset_tag_identity'),
trans('general.import-history'),
'asset_tag',
trans('admin/hardware/message.import.history.missing_asset_tag_message'),
);
return;
}
$asset = Asset::where('asset_tag', '=', $asset_tag)->first();
if (! $asset) {
$this->log('Asset '.$asset_tag.' does not exist - skipping');
$this->recordSkipped();
$this->addRowError(
trans('general.asset').' '.$asset_tag,
trans('general.import-history'),
'asset_tag',
trans('admin/hardware/message.import.history.asset_not_found_message'),
);
return;
}
$checkout_date = Carbon::parse($checkout_date_raw)->format('Y-m-d H:i:s');
$checkin_date = null;
if ($checkinHeaderPresent) {
if (! empty($checkin_date_raw)) {
$checkin_date = Carbon::parse($checkin_date_raw)->format('Y-m-d H:i:s');
}
} else {
// No checkin column in the header - assume already checked in
// as of import time so we don't leave the asset in a state that
// implies an open, indefinite checkout.
$checkin_date = Carbon::parse(now())->format('Y-m-d H:i:s');
}
$user = $this->resolveTargetUser($name);
if (! $user) {
$this->log('User "'.$name.'" does not exist so no checkin log was created for asset '.$asset_tag);
$this->recordSkipped();
$this->addRowError(
trans('general.asset').' '.$asset_tag,
trans('general.import-history'),
'name',
trans('admin/hardware/message.import.history.user_not_matched_message', ['name' => $name]),
);
return;
}
Actionlog::firstOrCreate([
'item_id' => $asset->id,
'item_type' => Asset::class,
'created_by' => $this->created_by,
'note' => 'Checkout imported by '.(auth()->user()?->display_name ?? 'CSV importer').' from history importer',
'target_id' => $user->id,
'target_type' => User::class,
'created_at' => $checkout_date,
'action_type' => 'checkout',
]);
// If the CSV explicitly told us about a checkin column and this row's
// checkin is empty or in the future, the asset is still assigned to
// the target user. Otherwise leave the current assignment alone -
// the asset is (by the source-of-truth CSV) already checked in.
if ($checkinHeaderPresent) {
if (empty($checkin_date) || strtotime($checkin_date) > strtotime(Carbon::now())) {
$asset->assigned_to = $user->id;
$asset->assigned_type = User::class;
}
}
if (! empty($checkin_date)) {
Actionlog::firstOrCreate([
'item_id' => $asset->id,
'item_type' => Asset::class,
'created_by' => $this->created_by,
'note' => 'Checkin imported by '.(auth()->user()?->display_name ?? 'CSV importer').' from history importer',
'target_id' => null,
'created_at' => $checkin_date,
'action_type' => 'checkin',
]);
}
if ($asset->save()) {
$this->log('Asset history imported for '.$asset_tag.' -> '.$user->username.' at '.$checkout_date);
$this->recordCreated();
} else {
$this->recordErrored();
$this->logError($asset, 'asset_tag');
}
}
/**
* Push a row-level error onto the same bag the standard CRUD
* importers use, so the wizard's post-run error table surfaces the
* skip reason to the user instead of hiding it in laravel.log.
*
* ItemImportRequest::errorCallback signature is
* ($item, $field, $errorString), reads $item->name for the row
* identity, and stores as errors[$item->name][$field] = $errorString.
* The wizard's error table then iterates $errorString as an inner
* [innerField => [message strings]] map, so we hand it that exact
* shape here. Wraps with a stdClass because we don't have a real
* model at row-error time.
*
* Bypasses the base addErrorToBag() helper - that one wraps the
* message in an extra [$field => [...]] layer which produces
* quadruple-nested arrays and blows up the blade's implode(). Direct
* callback invocation matches what logError() effectively produces
* from validating models.
*/
private function addRowError(string $identity, string $tableLabel, string $field, string $message): void
{
if (! $this->errorCallback) {
return;
}
$fake = new \stdClass;
$fake->name = $identity;
call_user_func(
$this->errorCallback,
$fake,
$tableLabel,
[$field => [$message]],
);
}
private function resolveTargetUser(?string $name): ?User
{
if (empty($name)) {
return null;
}
$base = User::generateFormattedNameFromFullName(Setting::getSettings()->username_format, $name);
$query = User::where('username', '=', $base['username']);
if ($this->matchFirstnameLastname) {
$firstDotLast = User::generateFormattedNameFromFullName('firstname.lastname', $name);
$query->orWhere('username', '=', $firstDotLast['username']);
}
if ($this->matchFlastname) {
$flastname = User::generateFormattedNameFromFullName('filastname', $name);
$query->orWhere('username', '=', $flastname['username']);
}
if ($this->matchFirstname) {
$firstname = User::generateFormattedNameFromFullName('firstname', $name);
$query->orWhere('username', '=', $firstname['username']);
}
if ($this->matchEmail) {
$query->orWhere('email', '=', User::generateEmailFromFullName($name));
}
if ($this->matchUsername) {
$query->orWhere('username', '=', $name);
}
return $query->first();
}
}