mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
261 lines
8.8 KiB
PHP
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();
|
|
}
|
|
}
|