Merge branch 'develop' into 17369-accessory-checkout-qty-scaffold

# Conflicts:
#	app/Http/Controllers/Account/AcceptanceController.php
#	resources/views/notifications/markdown/asset-acceptance.blade.php
This commit is contained in:
Marcus Moore
2025-08-26 11:39:52 -07:00
60 changed files with 19368 additions and 202 deletions

View File

@@ -4198,6 +4198,24 @@
"contributions": [
"code"
]
},
{
"login": "FlorestanII",
"name": "Johannes Pollitt",
"avatar_url": "https://avatars.githubusercontent.com/u/15015119?v=4",
"profile": "https://github.com/FlorestanII",
"contributions": [
"code"
]
},
{
"login": "strobelm",
"name": "Michael Strobel",
"avatar_url": "https://avatars.githubusercontent.com/u/14185442?v=4",
"profile": "https://strobelm.de",
"contributions": [
"code"
]
}
]
}

View File

@@ -68,7 +68,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/181059?v=4" width="110px;"/><br /><sub>Juan Font</sub>](https://github.com/juanfont)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juanfont "Code") | [<img src="https://avatars.githubusercontent.com/u/13137708?v=4" width="110px;"/><br /><sub>Juho Taipale</sub>](https://github.com/juhotaipale)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juhotaipale "Code") | [<img src="https://avatars.githubusercontent.com/u/1007419?v=4" width="110px;"/><br /><sub>Korvin Szanto</sub>](https://github.com/KorvinSzanto)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KorvinSzanto "Code") | [<img src="https://avatars.githubusercontent.com/u/8513053?v=4" width="110px;"/><br /><sub>Lewis Foster</sub>](https://lewisfoster.foo/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sniff122 "Code") | [<img src="https://avatars.githubusercontent.com/u/33877541?v=4" width="110px;"/><br /><sub>Logan Swartzendruber</sub>](https://github.com/loganswartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=loganswartz "Code") | [<img src="https://avatars.githubusercontent.com/u/1156208?v=4" width="110px;"/><br /><sub>Lorenzo P.</sub>](https://github.com/lopezio)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lopezio "Code") | [<img src="https://avatars.githubusercontent.com/u/33946590?v=4" width="110px;"/><br /><sub>Lukas Jung</sub>](https://github.com/m4us1ne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=m4us1ne "Code") |
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -138,13 +138,13 @@ class Handler extends ExceptionHandler
if (in_array('bulkedit', $ids, true)) {
$error_array = session()->get('bulk_asset_errors');
return redirect()
->route('hardware.bulkedit')
->route('hardware.index')
->withErrors($error_array, 'bulk_asset_errors')
->withInput();
}
// This gets the MVC model name from the exception and formats in a way that's less fugly
$model_name = strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel())))));
// This gets the MVC model name from the exception and formats in a way that's less fugly
$model_name = trim(strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
// Sigh.
@@ -160,9 +160,7 @@ class Handler extends ExceptionHandler
$route = 'maintenances.index';
} elseif ($route === 'licenseseats.index') {
$route = 'licenses.index';
} elseif ($route === 'customfields.index') {
$route = 'fields.index';
} elseif ($route === 'customfieldsets.index') {
} elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
$route = 'fields.index';
}

View File

@@ -123,9 +123,9 @@ class Helper
if (is_numeric($cost)) {
if (Setting::getSettings()->digit_separator=='1.234,56') {
return (float) number_format($cost, 2, ',', '.');
return number_format($cost, 2, ',', '.');
}
return (float) number_format($cost, 2, '.', ',');
return number_format($cost, 2, '.', ',');
}
// It's already been parsed.
return $cost;
@@ -1197,19 +1197,30 @@ class Helper
'webp' => 'far fa-image',
'avif' => 'far fa-image',
'svg' => 'fas fa-vector-square',
// word
'doc' => 'far fa-file-word',
'docx' => 'far fa-file-word',
// Excel
'xls' => 'far fa-file-excel',
'xlsx' => 'far fa-file-excel',
'ods' => 'far fa-file-excel',
// Presentation
'ppt' => 'far fa-file-powerpoint',
'odp' => 'far fa-file-powerpoint',
// archive
'zip' => 'fas fa-file-archive',
'rar' => 'fas fa-file-archive',
//Text
'odt' => 'far fa-file-alt',
'txt' => 'far fa-file-alt',
'rtf' => 'far fa-file-alt',
'xml' => 'fas fa-code',
// Misc
'pdf' => 'far fa-file-pdf',
'lic' => 'far fa-save',

View File

@@ -29,7 +29,7 @@ class StorageHelper
public static function getMediaType($file_with_path) {
// The file exists and is allowed to be displayed inline
// Get the file extension and determine the media type
if (Storage::exists($file_with_path)) {
$fileinfo = pathinfo($file_with_path);
$extension = strtolower($fileinfo['extension']);
@@ -51,6 +51,15 @@ class StorageHelper
case 'webm':
case 'mov':
return 'video';
case 'doc':
case 'docx':
return 'document';
case 'txt':
return 'text';
case 'xls':
case 'xlsx':
case 'ods':
return 'spreadsheet';
default:
return $extension; // Default for unknown types
}

View File

@@ -232,6 +232,7 @@ class AcceptanceController extends Controller
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
'date_settings' => $branding_settings->date_display_format,
'admin' => auth()->user()->present()?->fullName,
'qty' => $acceptance->qty ?? 1,
];

View File

@@ -117,15 +117,20 @@ class AssetsController extends Controller
'jobtitle',
];
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
foreach ($all_custom_fields as $field) {
$allowed_columns[] = $field->db_column_name();
}
$filter = [];
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
}
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
foreach ($all_custom_fields as $field) {
$allowed_columns[] = $field->db_column_name();
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
}
$assets = Asset::select('assets.*')
@@ -141,6 +146,7 @@ class AssetsController extends Controller
'model.category',
'model.manufacturer',
'model.fieldset',
'model.depreciation',
'supplier'
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.

View File

@@ -195,7 +195,7 @@ class ImportController extends Controller
// Run a backup immediately before processing
if ($request->get('run-backup')) {
Log::debug('Backup manually requested via importer');
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H:i:s')]);
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H-i-s')]);
} else {
Log::debug('NO BACKUP requested via importer');
}

View File

@@ -424,6 +424,9 @@ class AssetsController extends Controller
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->element == 'checkbox' && !$request->has($field->db_column)) {
$asset->{$field->db_column} = null;
}
if ($request->has($field->db_column)) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {

View File

@@ -544,7 +544,7 @@ class BulkAssetsController extends Controller
session()->put('bulk_asset_errors',$error_array);
return redirect()
->route('hardware.bulkedit')
->route('hardware.index')
->with('bulk_asset_errors', $error_array)
->withInput();
}

View File

@@ -144,10 +144,9 @@ class CustomFieldsController extends Controller
*/
public function deleteFieldFromFieldset($field_id, $fieldset_id) : RedirectResponse
{
$this->authorize('update', CustomField::class);
$field = CustomField::find($field_id);
$this->authorize('update', $field);
// Check that the field exists - this is mostly related to the demo, where we
// rewrite the data every x minutes, so it's possible someone might be disassociating
// a field from a fieldset just as we're wiping the database
@@ -157,11 +156,12 @@ class CustomFieldsController extends Controller
return redirect()->route('fieldsets.show', ['fieldset' => $fieldset_id])
->with('success', trans('admin/custom_fields/message.field.delete.success'));
} else {
return redirect()->back()->withErrors(['message' => "Field is in use and cannot be deleted."]);
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'))
->withInput();
}
}
return redirect()->back()->withErrors(['message' => "Error deleting field from fieldset"]);
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'));
}
@@ -172,20 +172,16 @@ class CustomFieldsController extends Controller
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v1.8]
*/
public function destroy($field_id) : RedirectResponse
public function destroy(CustomField $field) : RedirectResponse
{
if ($field = CustomField::find($field_id)) {
$this->authorize('delete', $field);
$this->authorize('delete', CustomField::class);
if (($field->fieldset) && ($field->fieldset->count() > 0)) {
return redirect()->back()->withErrors(['message' => 'Field is in-use']);
}
$field->delete();
return redirect()->route("fields.index")
->with("success", trans('admin/custom_fields/message.field.delete.success'));
if (($field->fieldset) && ($field->fieldset->count() > 0)) {
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.in_use'));
}
return redirect()->back()->withErrors(['message' => 'Field does not exist']);
$field->delete();
return redirect()->route("fields.index")
->with("success", trans('admin/custom_fields/message.field.delete.success'));
}
@@ -198,7 +194,7 @@ class CustomFieldsController extends Controller
*/
public function edit(Request $request, CustomField $field) : View | RedirectResponse
{
$this->authorize('update', $field);
$this->authorize('update', CustomField::class);
$fieldsets = CustomFieldset::get();
$customFormat = '';
if ((stripos($field->format, 'regex') === 0) && ($field->format !== CustomField::PREDEFINED_FORMATS['MAC'])) {
@@ -228,7 +224,7 @@ class CustomFieldsController extends Controller
*/
public function update(CustomFieldRequest $request, CustomField $field) : RedirectResponse
{
$this->authorize('update', $field);
$this->authorize('update', CustomField::class);
$show_in_email = $request->get("show_in_email", 0);
$display_in_user_view = $request->get("display_in_user_view", 0);
@@ -265,7 +261,6 @@ class CustomFieldsController extends Controller
if ($field->save()) {
// Sync fields with fieldsets
$fieldset_array = $request->input('associate_fieldsets');
if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) {

View File

@@ -26,7 +26,6 @@ class SecurityHeaders
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Ugh. Feature-Policy is dumb and clumsy and mostly irrelevant for Snipe-IT,
// since we don't provide any way to IFRAME anything in in the first place.

View File

@@ -58,6 +58,13 @@ class AssetsTransformer
'id' => (int) $asset->model->manufacturer->id,
'name'=> e($asset->model->manufacturer->name),
] : null,
'depreciation' => (($asset->model) && ($asset->model->depreciation)) ? [
'id' => (int) $asset->model->depreciation->id,
'name'=> e($asset->model->depreciation->name),
'months'=> (int) $asset->model->depreciation->months,
'type'=> e($asset->model->depreciation->depreciation_type),
'minimum'=> ($asset->model->depreciation->depreciation_min) ? (int) $asset->model->depreciation->depreciation_min : null,
] : null,
'supplier' => ($asset->supplier) ? [
'id' => (int) $asset->supplier->id,
'name'=> e($asset->supplier->name),

View File

@@ -25,7 +25,7 @@ class ConsumablesTransformer
$array = [
'id' => (int) $consumable->id,
'name' => e($consumable->name),
'image' => ($consumable->image) ? Storage::disk('public')->url('consumables/'.e($consumable->image)) : null,
'image' => ($consumable->getImageUrl()) ? ($consumable->getImageUrl()) : null,
'category' => ($consumable->category) ? ['id' => $consumable->category->id, 'name' => e($consumable->category->name)] : null,
'company' => ($consumable->company) ? ['id' => (int) $consumable->company->id, 'name' => e($consumable->company->name)] : null,
'item_no' => e($consumable->item_no),

View File

@@ -45,7 +45,7 @@ class UploadedFilesTransformer
] : null,
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'),
'inlineable' => StorageHelper::allowSafeInline($file->uploads_file_path()),
'inlineable' => StorageHelper::allowSafeInline($file->uploads_file_path()) ?? false,
'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false),
];

View File

@@ -47,7 +47,7 @@ class CheckinAssetMail extends Mailable
return new Envelope(
from: $from,
subject: trans('mail.Asset_Checkin_Notification'),
subject: trans('mail.Asset_Checkin_Notification', ['tag' => $this->item->asset_tag]),
);
}

View File

@@ -125,7 +125,7 @@ class CheckoutAssetMail extends Mailable
private function getSubject(): string
{
if ($this->firstTimeSending) {
return trans('mail.Asset_Checkout_Notification');
return trans('mail.Asset_Checkout_Notification', ['tag' => $this->item->asset_tag]);
}
return trans('mail.unaccepted_asset_reminder');

View File

@@ -230,11 +230,16 @@ class Consumable extends SnipeModel
*/
public function getImageUrl()
{
// If there is a consumable image, use that
if ($this->image) {
return Storage::disk('public')->url(app('consumables_upload_path').$this->image);
}
return false;
// Otherwise check for a category image
} elseif (($this->category) && ($this->category->image)) {
return Storage::disk('public')->url(app('categories_upload_path').e($this->category->image));
}
return false;
}
/**

View File

@@ -0,0 +1,116 @@
<?php
namespace App\Models\Labels\Tapes\Dymo;
class LabelWriter_11354 extends LabelWriter
{
private const BARCODE1D_HEIGHT = 3.00;
private const BARCODE_MARGIN = 1.80;
private const TAG_SIZE = 2.80;
private const TITLE_SIZE = 2.80;
private const TITLE_MARGIN = 0.50;
private const FIELD_SIZE = 2.80;
private const FIELD_MARGIN = 0.15;
public function getUnit()
{
return 'mm';
}
public function getWidth()
{
return 57;
}
public function getHeight()
{
return 32;
}
public function getSupportAssetTag()
{
return true;
}
public function getSupport1DBarcode()
{
return true;
}
public function getSupport2DBarcode()
{
return true;
}
public function getSupportFields()
{
return 5;
}
public function getSupportLogo()
{
return false;
}
public function getSupportTitle()
{
return true;
}
public function preparePDF($pdf)
{
}
public function write($pdf, $record)
{
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
$currentY = $pa->y1;
$usableWidth = $pa->w;
$usableHeight = $pa->h;
// Wide 1D barcode on top
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$currentX, $currentY, $usableWidth, self::BARCODE1D_HEIGHT
);
$currentY += self::BARCODE1D_HEIGHT + self::BARCODE_MARGIN;
$usableHeight -= self::BARCODE1D_HEIGHT + self::BARCODE_MARGIN;
}
// 2D Barcode in left column
if ($record->has('barcode2d')) {
$barcodeSize = $usableHeight - self::TAG_SIZE;
static::writeText(
$pdf, $record->get('tag'),
$currentX, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'C',
$barcodeSize, self::TAG_SIZE, true, 0
);
static::write2DBarcode(
$pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
$currentX, $currentY,
$barcodeSize, $barcodeSize
);
$currentX += $barcodeSize + self::BARCODE_MARGIN;
$usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
}
// Right column
if ($record->has('title')) {
static::writeText(
$pdf, $record->get('title'),
$currentX, $currentY,
'freesans', 'b', self::TITLE_SIZE, 'L',
$usableWidth, self::TITLE_SIZE, true, 0
);
$currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
}
foreach ($record->get('fields') as $field) {
static::writeText(
$pdf, (($field['label']) ? $field['label'].' ' : '') . $field['value'],
$currentX, $currentY,
'freesans', '', self::FIELD_SIZE, 'L',
$usableWidth, self::FIELD_SIZE, true, 0, 0.3
);
$currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
}
}
}

View File

@@ -21,7 +21,7 @@ class SnipeModel extends Model
*/
public function setPurchaseCostAttribute($value)
{
if (is_float($value)) {
if (is_numeric($value)) {
//value is *already* a floating-point number. Just assign it directly
$this->attributes['purchase_cost'] = $value;
return;

View File

@@ -153,12 +153,21 @@ class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig
"primary" => AttributeMapping::constant(true)->ignoreWrite()
]],
'phoneNumbers' => [[
"value" => AttributeMapping::eloquent("phone"),
"display" => null,
"type" => AttributeMapping::constant("work")->ignoreWrite(),
"primary" => AttributeMapping::constant(true)->ignoreWrite()
]],
// Mobile and work phone numbers
'phoneNumbers' => [
[
"value" => AttributeMapping::eloquent("phone"),
"display" => null,
"type" => AttributeMapping::constant("work")->ignoreWrite(),
"primary" => AttributeMapping::constant(true)->ignoreWrite(),
],
[
"value" => AttributeMapping::eloquent("mobile"),
"display" => null,
"type" => AttributeMapping::constant("mobile")->ignoreWrite(),
"primary" => AttributeMapping::constant(false)->ignoreWrite()
]
],
'ims' => [[
"value" => null,

View File

@@ -29,6 +29,7 @@ class AcceptanceAssetAcceptedNotification extends Notification
$this->assigned_to = $params['assigned_to'];
$this->note = $params['note'];
$this->company_name = $params['company_name'];
$this->admin = $params['admin'] ?? null;
$this->settings = Setting::getSettings();
$this->qty = $params['qty'] ?? null;
@@ -74,6 +75,7 @@ class AcceptanceAssetAcceptedNotification extends Notification
'company_name' => $this->company_name,
'qty' => $this->qty,
'intro_text' => trans('mail.acceptance_asset_accepted'),
'admin' => $this->admin,
])
->subject(trans('mail.acceptance_asset_accepted'));

View File

@@ -85,7 +85,7 @@ abstract class SnipePermissionsPolicy
}
/**
* Determine whether the user can view the accessory.
* Determine whether the user can view the model.
*
* @param \App\Models\User $user
* @return mixed
@@ -101,7 +101,7 @@ abstract class SnipePermissionsPolicy
}
/**
* Determine whether the user can create accessories.
* Determine whether the user can create model.
*
* @param \App\Models\User $user
* @return mixed
@@ -112,7 +112,7 @@ abstract class SnipePermissionsPolicy
}
/**
* Determine whether the user can update the accessory.
* Determine whether the user can update the model.
*
* @param \App\Models\User $user
* @return mixed
@@ -124,7 +124,7 @@ abstract class SnipePermissionsPolicy
/**
* Determine whether the user can update the accessory.
* Determine whether the user can update the model.
*
* @param \App\Models\User $user
* @return mixed
@@ -135,7 +135,7 @@ abstract class SnipePermissionsPolicy
}
/**
* Determine whether the user can delete the accessory.
* Determine whether the user can delete the model.
*
* @param \App\Models\User $user
* @return mixed
@@ -151,7 +151,7 @@ abstract class SnipePermissionsPolicy
}
/**
* Determine whether the user can manage the accessory.
* Determine whether the user can manage the model.
*
* @param \App\Models\User $user
* @return mixed

View File

@@ -328,7 +328,7 @@ class AssetPresenter extends Presenter
// name can break the listings page. - snipe
foreach ($fields as $field) {
$layout[] = [
'field' => 'custom_fields.'.$field->db_column,
'field' => $field->db_column,
'searchable' => true,
'sortable' => true,
'switchable' => true,

View File

@@ -191,7 +191,7 @@ class ComponentPresenter extends Presenter
*/
public function nameUrl()
{
return (string) link_to_route('consumables.show', e($this->name), $this->id);
return (string) link_to_route('components.show', e($this->name), $this->id);
}
/**
@@ -200,6 +200,6 @@ class ComponentPresenter extends Presenter
*/
public function viewUrl()
{
return route('accessories.show', $this->id);
return route('components.show', $this->id);
}
}

View File

@@ -53,6 +53,7 @@ class LicensePresenter extends Presenter
'searchable' => true,
'sortable' => true,
'title' => trans('admin/licenses/form.to_email'),
'formatter' => 'emailFormatter',
], [
'field' => 'license_name',
'searchable' => true,

View File

@@ -37,7 +37,7 @@
"doctrine/dbal": "^3.1",
"doctrine/instantiator": "^1.3",
"eduardokum/laravel-mail-auto-embed": "^2.0",
"enshrined/svg-sanitize": "^0.16.0",
"enshrined/svg-sanitize": "^0.22.0",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.24",
"guzzlehttp/guzzle": "^7.0.1",

80
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "75fb4f46ea0a488c2dd45d73eb2a9b9d",
"content-hash": "80c3f4268ff9cda7df9ad90a8b11ff50",
"packages": [
{
"name": "alek13/slack",
@@ -1678,26 +1678,25 @@
},
{
"name": "enshrined/svg-sanitize",
"version": "0.16.0",
"version": "0.22.0",
"source": {
"type": "git",
"url": "https://github.com/darylldoyle/svg-sanitizer.git",
"reference": "239e257605e2141265b429e40987b2ee51bba4b4"
"reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/239e257605e2141265b429e40987b2ee51bba4b4",
"reference": "239e257605e2141265b429e40987b2ee51bba4b4",
"url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/0afa95ea74be155a7bcd6c6fb60c276c39984500",
"reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ezyang/htmlpurifier": "^4.16",
"php": "^5.6 || ^7.0 || ^8.0"
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7 || ^6.5 || ^8.5"
"phpunit/phpunit": "^6.5 || ^8.5"
},
"type": "library",
"autoload": {
@@ -1718,9 +1717,9 @@
"description": "An SVG sanitizer for PHP",
"support": {
"issues": "https://github.com/darylldoyle/svg-sanitizer/issues",
"source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.16.0"
"source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.22.0"
},
"time": "2023-03-20T10:51:12+00:00"
"time": "2025-08-12T10:13:48+00:00"
},
{
"name": "erusev/parsedown",
@@ -1772,67 +1771,6 @@
},
"time": "2019-12-30T22:54:17+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.18.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "cb56001e54359df7ae76dc522d08845dc741621b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b",
"reference": "cb56001e54359df7ae76dc522d08845dc741621b",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0"
},
"time": "2024-11-01T03:51:45+00:00"
},
{
"name": "fakerphp/faker",
"version": "v1.24.1",

View File

@@ -123,6 +123,9 @@ $config['allowed_upload_extensions_array'] = [
'mov',
'mp3',
'mp4',
'odp',
'ods',
'odt',
'ogg',
'pdf',
'png',
@@ -140,12 +143,15 @@ $config['allowed_upload_extensions_array'] = [
];
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types
$config['allowed_upload_mimetypes_array'] = [
'application/json',
'application/msword',
'application/pdf',
'application/vnd.ms-excel',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.text',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/x-rar-compressed',

View File

@@ -351,6 +351,17 @@ class UserFactory extends Factory
return $this->appendPermission(['import' => '1']);
}
public function createCustomFields()
{
return $this->appendPermission(['customfields.create' => '1']);
}
public function viewCustomFields()
{
return $this->appendPermission(['customfields.view' => '1']);
}
public function deleteCustomFields()
{
return $this->appendPermission(['customfields.delete' => '1']);

View File

@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('categories', function (Blueprint $table) {
$table->index(['deleted_at']);
});
Schema::table('accessories', function (Blueprint $table) {
$table->index(['deleted_at','category_id']);
});
Schema::table('consumables', function (Blueprint $table) {
$table->index(['deleted_at','category_id']);
});
Schema::table('components', function (Blueprint $table) {
$table->index(['deleted_at','category_id']);
});
Schema::table('licenses', function (Blueprint $table) {
$table->index(['deleted_at','category_id']);
});
Schema::table('models', function (Blueprint $table) {
$table->index(['deleted_at','category_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('categories', function (Blueprint $table) {
$table->dropIndex(['deleted_at']);
});
Schema::table('accessories', function (Blueprint $table) {
$table->dropIndex(['deleted_at','category_id']);
});
Schema::table('consumables', function (Blueprint $table) {
$table->dropIndex(['deleted_at','category_id']);
});
Schema::table('components', function (Blueprint $table) {
$table->dropIndex(['deleted_at','category_id']);
});
Schema::table('licenses', function (Blueprint $table) {
$table->dropIndex(['deleted_at','category_id']);
});
Schema::table('models', function (Blueprint $table) {
$table->dropIndex(['deleted_at','category_id']);
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('models', function (Blueprint $table) {
$table->index(['created_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('models', function (Blueprint $table) {
$table->dropIndex(['created_at']);
});
}
};

View File

@@ -1072,7 +1072,6 @@ th.css-house-laptop > .th-inner::before,
th.css-house-user > .th-inner::before,
th.css-license > .th-inner::before,
th.css-location > .th-inner::before,
th.css-padlock > .th-inner::before,
th.css-users > .th-inner::before,
th.css-currency > .th-inner::before,
th.css-history > .th-inner::before {
@@ -1129,7 +1128,8 @@ th.css-component > .th-inner::before {
th.css-padlock > .th-inner::before {
content: "\f023";
font-family: "Font Awesome 5 Free";
font-weight: 900;
font-weight: 800;
padding-right: 3px;
}
th.css-house-user > .th-inner::before {
content: "\e1b0";
@@ -1480,6 +1480,13 @@ caption.tableCaption {
margin-left: -47px;
margin-top: 2px;
}
.popover.help-popover,
.popover.help-popover .popover-content,
.popover.help-popover .popover-body,
.popover.help-popover .popover-title,
.popover.help-popover .popover-header {
color: #000;
}
/*# sourceMappingURL=app.css.map*/

File diff suppressed because one or more lines are too long

View File

@@ -693,7 +693,6 @@ th.css-house-laptop > .th-inner::before,
th.css-house-user > .th-inner::before,
th.css-license > .th-inner::before,
th.css-location > .th-inner::before,
th.css-padlock > .th-inner::before,
th.css-users > .th-inner::before,
th.css-currency > .th-inner::before,
th.css-history > .th-inner::before {
@@ -750,7 +749,8 @@ th.css-component > .th-inner::before {
th.css-padlock > .th-inner::before {
content: "\f023";
font-family: "Font Awesome 5 Free";
font-weight: 900;
font-weight: 800;
padding-right: 3px;
}
th.css-house-user > .th-inner::before {
content: "\e1b0";
@@ -1101,6 +1101,13 @@ caption.tableCaption {
margin-left: -47px;
margin-top: 2px;
}
.popover.help-popover,
.popover.help-popover .popover-content,
.popover.help-popover .popover-body,
.popover.help-popover .popover-title,
.popover.help-popover .popover-header {
color: #000;
}
/*# sourceMappingURL=overrides.css.map*/

File diff suppressed because one or more lines are too long

View File

@@ -22408,7 +22408,6 @@ th.css-house-laptop > .th-inner::before,
th.css-house-user > .th-inner::before,
th.css-license > .th-inner::before,
th.css-location > .th-inner::before,
th.css-padlock > .th-inner::before,
th.css-users > .th-inner::before,
th.css-currency > .th-inner::before,
th.css-history > .th-inner::before {
@@ -22465,7 +22464,8 @@ th.css-component > .th-inner::before {
th.css-padlock > .th-inner::before {
content: "\f023";
font-family: "Font Awesome 5 Free";
font-weight: 900;
font-weight: 800;
padding-right: 3px;
}
th.css-house-user > .th-inner::before {
content: "\e1b0";
@@ -22816,6 +22816,13 @@ caption.tableCaption {
margin-left: -47px;
margin-top: 2px;
}
.popover.help-popover,
.popover.help-popover .popover-content,
.popover.help-popover .popover-body,
.popover.help-popover .popover-title,
.popover.help-popover .popover-header {
color: #000;
}
/*# sourceMappingURL=app.css.map*/
@@ -23996,7 +24003,6 @@ th.css-house-laptop > .th-inner::before,
th.css-house-user > .th-inner::before,
th.css-license > .th-inner::before,
th.css-location > .th-inner::before,
th.css-padlock > .th-inner::before,
th.css-users > .th-inner::before,
th.css-currency > .th-inner::before,
th.css-history > .th-inner::before {
@@ -24053,7 +24059,8 @@ th.css-component > .th-inner::before {
th.css-padlock > .th-inner::before {
content: "\f023";
font-family: "Font Awesome 5 Free";
font-weight: 900;
font-weight: 800;
padding-right: 3px;
}
th.css-house-user > .th-inner::before {
content: "\e1b0";
@@ -24404,6 +24411,13 @@ caption.tableCaption {
margin-left: -47px;
margin-top: 2px;
}
.popover.help-popover,
.popover.help-popover .popover-content,
.popover.help-popover .popover-body,
.popover.help-popover .popover-title,
.popover.help-popover .popover-header {
color: #000;
}
/*# sourceMappingURL=overrides.css.map*/

18819
public/js/dist/all.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,9 @@
{
"/js/dist/all.js": "/js/dist/all.js?id=b4f9d0394ed0703585d495cf5995a169",
"/js/dist/all.js": "/js/dist/all.js?id=76d88f0f91b852f7eecbce357ab5858b",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=42f97cd5b9ee7521b04a448e7fc16ac9",
"/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=d81a7ed323f68a7c5e3e9115f7fb5404",
"/css/build/overrides.css": "/css/build/overrides.css?id=d854d9c3eedb56197e6286be06eda5d7",
"/css/build/app.css": "/css/build/app.css?id=b679be8335a9d2e994b28a402c92e2b2",
"/css/build/overrides.css": "/css/build/overrides.css?id=81e3e83b8d669c69d443a21241184f2e",
"/css/build/app.css": "/css/build/app.css?id=b34e2d41a3a0d0c949d813eabdc0204b",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=ee0ed88465dd878588ed044eefb67723",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=3d8a3d2035ea28aaad4a703c2646f515",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=3979929a3423ff35b96b1fc84299fdf3",
@@ -19,7 +19,7 @@
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=b2cd9f59d7e8587939ce27b2d3363d82",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=7277edd636cf46aa7786a4449ce0ead7",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=cbd06cc1d58197ccc81d4376bbaf0d28",
"/css/dist/all.css": "/css/dist/all.css?id=cc4771fafe9ab1a69b1f45d20f57342d",
"/css/dist/all.css": "/css/dist/all.css?id=ddd108d2798f64c3a47dc2e61760af12",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",

View File

@@ -11,7 +11,7 @@ window.$ = jQuery
itself
*****************************************/
require('jquery-ui'); //should we export this to the window?
require("jquery-ui/dist/jquery-ui")
jQuery.fn.uitooltip = jQuery.fn.tooltip;
require('bootstrap-less');
require('select2');

View File

@@ -769,7 +769,6 @@ th.css-house-laptop > .th-inner::before,
th.css-house-user > .th-inner::before,
th.css-license > .th-inner::before,
th.css-location > .th-inner::before,
th.css-padlock > .th-inner::before,
th.css-users > .th-inner::before,
th.css-currency > .th-inner::before,
th.css-history > .th-inner::before
@@ -824,7 +823,9 @@ th.css-component > .th-inner::before
th.css-padlock > .th-inner::before
{
content: "\f023"; font-family: "Font Awesome 5 Free"; font-weight: 900;
content: "\f023"; font-family: "Font Awesome 5 Free";
font-weight: 800;
padding-right: 3px;
}
th.css-house-user > .th-inner::before {
@@ -1234,3 +1235,11 @@ caption.tableCaption {
margin-left: -47px;
margin-top: 2px;
}
.popover.help-popover,
.popover.help-popover .popover-content,
.popover.help-popover .popover-body,
.popover.help-popover .popover-title,
.popover.help-popover .popover-header {
color: #000;
}

View File

@@ -337,6 +337,7 @@ return [
'zip' => 'Zip',
'noimage' => 'No image uploaded or image not found.',
'file_does_not_exist' => 'The requested file does not exist on the server.',
'file_not_inlineable' => 'The requested file cannot be opened inline in your browser. You can download it instead.',
'open_new_window' => 'Open this file in a new window',
'file_upload_success' => 'File upload success!',
'no_files_uploaded' => 'File upload success!',

View File

@@ -4,8 +4,8 @@ return [
'Accessory_Checkin_Notification' => 'Accessory checked in',
'Accessory_Checkout_Notification' => 'Accessory checked out',
'Asset_Checkin_Notification' => 'Asset checked in',
'Asset_Checkout_Notification' => 'Asset checked out',
'Asset_Checkin_Notification' => 'Asset checked in: [:tag]',
'Asset_Checkout_Notification' => 'Asset checked out: [:tag]',
'Confirm_Accessory_Checkin' => 'Accessory checkin confirmation',
'Confirm_Asset_Checkin' => 'Asset checkin confirmation',
'Confirm_component_checkin' => 'Component checkin confirmation',

View File

@@ -119,9 +119,9 @@
<td>
@if ($requestableModel->image)
<a href="{{ config('app.url') }}/uploads/models/{{ $requestableModel->image }}" data-toggle="lightbox" data-type="image">
<img src="{{ config('app.url') }}/uploads/models/{{ $requestableModel->image }}" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive">
@if (($requestableModel->image) && ($requestableModel->getImageUrl()))
<a href="{{ $requestableModel->getImageUrl() }}" data-toggle="lightbox" data-type="image">
<img src="{{ $requestableModel->getImageUrl() }}" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive">
</a>
@endif

View File

@@ -84,15 +84,12 @@
@endcan
@can('delete', $fieldset)
<form method="POST" action="{{ route('fieldsets.destroy', $fieldset->id) }}" accept-charset="UTF-8" style="display:inline-block">
{{ method_field('DELETE') }}
@csrf
@if($fieldset->models->count() > 0)
<button type="submit" class="btn btn-danger btn-sm disabled" data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}" disabled><i class="fas fa-trash"></i></button>
@else
<button type="submit" class="btn btn-danger btn-sm delete-asset" data-tooltip="true" title="{{ trans('general.delete') }}" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.sure_to_delete_var', ['item' => $fieldset->name]) }}" data-icon="fa fa-trash" data-target="#dataConfirmModal" onClick="return false;"><i class="fas fa-trash"></i></button>
<a type="submit" href="{{ route('fieldsets.destroy', $fieldset) }}" class="btn btn-danger btn-sm delete-asset" data-tooltip="true" title="{{ trans('general.delete') }}" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.sure_to_delete_var', ['item' => $fieldset->name]) }}" data-icon="fa fa-trash" data-target="#dataConfirmModal" onClick="return false;"><i class="fas fa-trash"></i></a>
@endif
</form>
@endcan
</nobr>
</td>
@@ -237,9 +234,6 @@
</td>
<td>
<nobr>
<form method="POST" action="{{ route('fields.destroy', $field->id) }}" accept-charset="UTF-8" style="display:inline-block">
{{ method_field('DELETE') }}
@csrf
@can('update', $field)
<a href="{{ route('fields.edit', $field->id) }}" class="btn btn-warning btn-sm" data-tooltip="true" title="{{ trans('general.update') }}">
<i class="fas fa-pencil-alt" aria-hidden="true"></i>
@@ -249,19 +243,19 @@
@can('delete', $field)
@if($field->fieldset->count()>0)
@if ($field->fieldset->count() > 0)
<button type="submit" class="btn btn-danger btn-sm disabled" data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}" disabled>
<i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.delete') }}</span></button>
@else
<button type="submit" class="btn btn-danger btn-sm delete-asset" data-tooltip="true" title="{{ trans('general.delete') }}" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.sure_to_delete_var', ['item' => $field->name]) }}" data-target="#dataConfirmModal" data-icon="fa fa-trash" onClick="return false;">
<i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.delete') }}</span>
</button>
@else
<a href="{{ route('fields.destroy', $field) }}" class="btn btn-danger btn-sm delete-asset" data-tooltip="true" title="{{ trans('general.delete') }}" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.sure_to_delete_var', ['item' => $field->name]) }}" data-target="#dataConfirmModal" data-icon="fa fa-trash" onClick="return false;">
<i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">{{ trans('button.delete') }}</span>
</a>
@endif
@endcan
</form>
</nobr>
</td>
</tr>

View File

@@ -809,7 +809,7 @@
<div class="col-md-9">
{{ Helper::getFormattedDateObject($asset->purchase_date, 'date', false) }}
-
{{ Carbon::parse($asset->purchase_date)->diff(Carbon::now())->format('%y years, %m months and %d days')}}
{{ Carbon::parse($asset->purchase_date)->diffForHumans(['parts' => 3]) }}
</div>
</div>
@@ -930,9 +930,7 @@
<div class="col-md-3">
<strong>
{{ trans('admin/hardware/form.warranty_expires') }}
@if ($asset->purchase_date)
{!! $asset->present()->warranty_expires() < date("Y-m-d") ? '<i class="fas fa-exclamation-triangle text-orange" aria-hidden="true"></i>' : '' !!}
@endif
</strong>
</div>
@@ -940,7 +938,11 @@
@if ($asset->purchase_date)
{{ Helper::getFormattedDateObject($asset->present()->warranty_expires(), 'date', false) }}
-
{{ Carbon::parse($asset->present()->warranty_expires())->diffForHumans(['parts' => 2]) }}
{{ Carbon::parse($asset->present()->warranty_expires())->diffForHumans(['parts' => 3]) }}
@if ($asset->purchase_date)
{!! $asset->present()->warranty_expires() < date("Y-m-d") ? '<i class="fas fa-exclamation-triangle text-orange" aria-hidden="true"></i>' : '' !!}
@endif
@else
{{ trans('general.na_no_purchase_date') }}
@endif
@@ -971,7 +973,7 @@
@if ($asset->purchase_date)
{{ Helper::getFormattedDateObject($asset->depreciated_date()->format('Y-m-d'), 'date', false) }}
-
{{ Carbon::parse($asset->depreciated_date())->diffForHumans(['parts' => 2]) }}
{{ Carbon::parse($asset->depreciated_date())->diffForHumans(['parts' => 3]) }}
@else
{{ trans('general.na_no_purchase_date') }}
@endif
@@ -1008,7 +1010,7 @@
@if ($asset->asset_eol_date)
{{ Helper::getFormattedDateObject($asset->asset_eol_date, 'date', false) }}
-
{{ Carbon::parse($asset->asset_eol_date)->diffForHumans(['parts' => 2]) }}
{{ Carbon::parse($asset->asset_eol_date)->locale(app()->getLocale())->diffForHumans(['parts' => 3]) }}
@else
{{ trans('general.na_no_purchase_date') }}
@endif
@@ -1017,7 +1019,7 @@
data-placement="top"
data-title="Explicit EOL"
title="Explicit EOL">
<x-icon type="warning" class="text-orange" />
<x-icon type="warning" class="text-primary" />
</span>
@endif
</div>

View File

@@ -14,6 +14,12 @@
| **{{ trans('mail.checkout_date') }}** | {{ $checkout_date }} |
@endif
| **{{ trans('general.accessory') }}** | {{ $item->name }} |
@if (isset($item->category))
| **{{ trans('general.category') }}** | {{ $item->category->name }} |
@endif
@if (isset($item->model_number))
| **{{ trans('general.model_no') }}** | {{ $item->model_number }} |
@endif
@if (isset($item->manufacturer))
| **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} |
@endif

View File

@@ -9,7 +9,11 @@
<form action="{{ route('api.locations.store') }}" onsubmit="return false">
<div class="alert alert-danger" id="modal_error_msg" style="display:none">
</div>
@include('modals.partials.name', ['item' => new \App\Models\Location(), 'required' => 'true'])
<div class="dynamic-form-row">
<div class="col-md-3 col-xs-12"><label for="modal-name">{{ trans('general.name') }}:</label></div>
<div class="col-md-9 col-xs-12"><input type='text' name="name" id='modal-name' class="form-control"></div>
</div>
<!-- Setup of default company, taken from asset creator if scoped locations are activated in the settings -->
@if (($snipeSettings->scope_locations_fmcs == '1') && ($user->company))
@@ -22,13 +26,13 @@
</div>
<div class="dynamic-form-row">
<div class="col-md-4 col-xs-12"><label for="modal-city">{{ trans('general.city') }}:</label></div>
<div class="col-md-8 col-xs-12"><input type='text' name="city" id='modal-city' class="form-control"></div>
<div class="col-md-3 col-xs-12"><label for="modal-city">{{ trans('general.city') }}:</label></div>
<div class="col-md-9 col-xs-12"><input type='text' name="city" id='modal-city' class="form-control"></div>
</div>
<div class="dynamic-form-row">
<div class="col-md-4 col-xs-12 country"><label for="modal-country">{{ trans('general.country') }}:</label></div>
<div class="col-md-8 col-xs-12">{!! Form::countries('country', old('country'), 'select2 country',"modal-country") !!}</div>
<div class="col-md-3 col-xs-12 country"><label for="modal-country">{{ trans('general.country') }}:</label></div>
<div class="col-md-9 col-xs-12">{!! Form::countries('country', old('country'), 'select2 country',"modal-country") !!}</div>
</div>
</form>
</div>

View File

@@ -40,6 +40,9 @@
@if (isset($qty))
| **{{ trans('general.qty') }}** | {{ $qty }} |
@endif
@if (isset($admin))
| **{{ trans('general.administrator') }}** | {{ $admin }} |
@endif
@endcomponent
{{ trans('mail.best_regards') }}

View File

@@ -3,14 +3,14 @@
@component('mail::table')
<table width="100%">
<tr><td>&nbsp;</td><td>{{ trans('mail.name') }}</td><td>{{ trans('mail.Days') }}</td><td>{{ trans('mail.expires') }}</td><td>{{ trans('mail.supplier') }}</td><td>{{ trans('mail.assigned_to') }}</td></tr>
<tr><td>&nbsp;</td><td>{{ trans('mail.name') }}</small></td><td>{{ trans('mail.serial') }}</td><td>{{ trans('mail.Days') }}</td><td>{{ trans('mail.expires') }}</td><td>{{ trans('mail.supplier') }}</td><td>{{ trans('mail.assigned_to') }}</td></tr>
@foreach ($assets as $asset)
@php
$expires = Helper::getFormattedDateObject($asset->present()->warranty_expires, 'date');
$diff = round(abs(strtotime($asset->present()->warranty_expires) - strtotime(date('Y-m-d')))/86400);
$icon = ($diff <= ($threshold / 2)) ? '🚨' : (($diff <= $threshold) ? '⚠️' : ' ');
@endphp
<tr><td>{{ $icon }} </td><td> <a href="{{ route('hardware.show', $asset->id) }}">{{ $asset->present()->name }}</a> </td><td> {{ $diff }} {{ trans('mail.Days') }} </td><td> {{ !is_null($expires) ? $expires['formatted'] : '' }} </td><td> {{ ($asset->supplier ? e($asset->supplier->name) : '') }} </td><td> {{ ($asset->assignedTo ? e($asset->assignedTo->present()->name()) : '') }} </td></tr>
<tr><td>{{ $icon }} </td><td> <a href="{{ route('hardware.show', $asset->id) }}">{{ $asset->present()->name }}</a><br><small>{{trans('mail.serial').': '.$asset->serial}}</small></td><td> {{ $diff }} {{ trans('mail.Days') }} </td><td> {{ !is_null($expires) ? $expires['formatted'] : '' }} </td><td> {{ ($asset->supplier ? e($asset->supplier->name) : '') }} </td><td> {{ ($asset->assignedTo ? e($asset->assignedTo->present()->name()) : '') }} </td></tr>
@endforeach
</table>
@endcomponent

View File

@@ -966,10 +966,14 @@
var download_button = '<a href="' + download_url + '" class="btn btn-sm btn-default" data-tooltip="true" title="{{ trans('general.download') }}"><x-icon type="download" /></a>';
var download_button_disabled = '<span data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><a class="btn btn-sm btn-default disabled"><x-icon type="download" /></a></span>';
var inline_button = '<a href="'+ download_url +'?inline=true" class="btn btn-sm btn-default" target="_blank" data-tooltip="true" title="{{ trans('general.open_new_window') }}"><x-icon type="external-link" /></a>';
var inline_button_disabled = '<span data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><a class="btn btn-sm btn-default disabled" target="_blank" data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="external-link" /></a></span>';
var inline_button_disabled = '<span data-tooltip="true" title="{{ trans('general.file_not_inlineable') }}"><a class="btn btn-sm btn-default disabled" target="_blank" data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="external-link" /></a></span>';
if (exists_on_disk === true) {
return '<span style="white-space: nowrap;">' + download_button + ' ' + inline_button + '</span>';
if (inlinable === true) {
return '<span style="white-space: nowrap;">' + download_button + ' ' + inline_button + '</span>';
} else {
return '<span style="white-space: nowrap;">' + download_button + ' ' + inline_button_disabled + '</span>';
}
} else {
return '<span style="white-space: nowrap;">' + download_button_disabled + ' ' + inline_button_disabled + '</span>';
}
@@ -1340,7 +1344,7 @@
window.location.href = '{{ route('maintenances.create', ['asset_id' => (isset($asset)) ? $asset->id :'' ]) }}';
},
attributes: {
title: '{{ trans('general.create') }}',
title: '{{ trans('button.add_maintenance') }}',
}
},
@endcan

View File

@@ -3,7 +3,7 @@
<label for="purchase_cost" class="col-md-3 control-label">{{ trans('general.purchase_cost') }}</label>
<div class="col-md-9">
<div class="input-group col-md-4" style="padding-left: 0px;">
<input class="form-control" type="number" name="purchase_cost" min="0.00" max="10000000.000" step="0.001" aria-label="purchase_cost" id="purchase_cost" value="{{ old('purchase_cost', Helper::formatCurrencyOutput($item->purchase_cost)) }}" maxlength="24" />
<input class="form-control" type="number" name="purchase_cost" min="0.00" max="10000000.000" step="0.001" aria-label="purchase_cost" id="purchase_cost" value="{{ old('purchase_cost', $item->purchase_cost) }}" maxlength="24" />
<span class="input-group-addon">
@if (isset($currency_type))
{{ $currency_type }}

View File

@@ -1,4 +1,6 @@
<a style="padding-left: 5px; font-size: 15px;" class="text-dark-gray hidden-print" data-trigger="focus" tabindex="0" role="button" data-toggle="popover" title="{{ trans('general.more_info') }}" data-placement="right" data-html="true" data-content="{{ (isset($helpText)) ? $helpText : 'Help Info Missing' }}">
<a style="padding-left: 5px; font-size: 15px;" class="text-dark-gray hidden-print" data-trigger="focus" tabindex="0" role="button" data-toggle="popover" data-container="body"
data-template="<div class='popover help-popover' role='tooltip'><div class='arrow'></div><h3 class='popover-title'></h3><div class='popover-content popover-body'></div></div>"
title="{{ trans('general.more_info') }}" data-placement="right" data-html="true" data-content="{{ (isset($helpText)) ? $helpText : 'Help Info Missing' }}">
<x-icon type="more-info" style="padding-top: 9px;" />
<span class="sr-only">{{ trans('general.moreinfo') }}</span>
</a>

View File

@@ -672,7 +672,7 @@
<span data-tooltip="true" title="{{ trans('general.delete') }}">
<a href="#"
class="btn btn-sm btn-danger btn-social btn-block"
class="btn btn-sm btn-danger btn-social btn-block delete-asset"
data-toggle="modal"
data-title="{{ trans('general.delete') }}"
data-content="{{ trans('general.delete_confirm', ['item' => $template->name]) }}"

View File

@@ -911,11 +911,12 @@
}'>
<thead>
<tr>
<th class="col-md-1">{{ trans('general.id') }}</th>
<th class="col-md-4">{{ trans('general.name') }}</th>
<th class="col-md-5" data-fieldname="note">{{ trans('general.notes') }}</th>
<th class="col-md-1" data-footer-formatter="sumFormatter" data-fieldname="purchase_cost">{{ trans('general.purchase_cost') }}</th>
<th class="col-md-1 hidden-print">{{ trans('general.action') }}</th>
<th>{{ trans('general.id') }}</th>
<th>{{ trans('general.name') }}</th>
<th>{{ trans('general.date') }}</th>
<th data-fieldname="note">{{ trans('general.notes') }}</th>
<th data-footer-formatter="sumFormatter" data-fieldname="purchase_cost">{{ trans('general.purchase_cost') }}</th>
<th class="hidden-print">{{ trans('general.action') }}</th>
</tr>
</thead>
<tbody>
@@ -923,6 +924,7 @@
<tr>
<td>{{ $accessory->pivot->id }}</td>
<td>{!!$accessory->present()->nameUrl()!!}</td>
<td>{{ Helper::getFormattedDateObject($accessory->pivot->created_at, 'datetime', false) }}</td>
<td>{!! $accessory->pivot->note !!}</td>
<td>
{!! Helper::formatCurrencyOutput($accessory->purchase_cost) !!}

View File

@@ -0,0 +1,69 @@
<?php
namespace Tests\Feature\CustomFields\Ui;
use App\Models\CustomField;
use App\Models\CustomFieldset;
use App\Models\User;
use Tests\TestCase;
class DeleteCustomFieldsTest extends TestCase
{
public function testPermissionNeededToDeleteField()
{
$this->actingAs(User::factory()->create())
->delete(route('fields.destroy', CustomField::factory()->create()))
->assertForbidden();
}
public function testCanDeleteCustomField()
{
$field = CustomField::factory()->create();
$this->assertDatabaseHas('custom_fields', ['id' => $field->id]);
$this->actingAs(User::factory()->deleteCustomFields()->create())
->delete(route('fields.destroy', $field))
->assertRedirectToRoute('fields.index')
->assertStatus(302)
->assertSessionHas('success');
$this->assertDatabaseMissing('custom_fields', ['id' => $field->id]);
}
public function testCannotDeleteCustomFieldThatDoesNotExist()
{
$response = $this->actingAs(User::factory()->viewCustomFields()->deleteCustomFields()->create())
->delete(route('fields.destroy', '49857589'))
->assertRedirect(route('fields.index'))
->assertSessionHas('error');
$temp = $this->followRedirects($response);
$temp->assertSee(trans('general.error'))->assertSee(trans('general.generic_model_not_found', ['model' => 'custom field']));
}
public function testCannotDeleteFieldThatIsAssociatedWithFieldsets()
{
$field = CustomField::factory()->create();
$fieldset = CustomFieldset::factory()->create();
$this->actingAs(User::factory()->superuser()->create())
->post(route('fieldsets.associate', $fieldset), [
'field_id' => $field->id,
]);
$response = $this->actingAs(User::factory()->viewCustomFields()->deleteCustomFields()->create())
->from(route('fields.index'))
->delete(route('fields.destroy', $field))
->assertStatus(302)
->assertRedirect(route('fields.index'))
->assertSessionHas('error');
$this->followRedirects($response)->assertSee(trans('general.error'))->assertSee(trans('admin/custom_fields/message.field.delete.in_use'));
// Ensure the field is still in the database
$this->assertDatabaseHas('custom_fields', ['id' => $field->id]);
}
}

View File

@@ -20,7 +20,7 @@ class CheckoutAssetMailTest extends TestCase
'asset' => $asset,
'acceptance' => CheckoutAcceptance::factory()->for($asset, 'checkoutable')->create(),
'first_time_sending' => true,
'expected_subject' => 'Asset checked out',
'expected_subject' => trans('mail.Asset_Checkout_Notification', ['tag' => $asset->asset_tag]),
'expected_opening' => 'A new item has been checked out under your name that requires acceptance, details are below.'
];
}
@@ -28,11 +28,12 @@ class CheckoutAssetMailTest extends TestCase
yield 'Asset not requiring acceptance' => [
function () {
$asset = Asset::factory()->doesNotRequireAcceptance()->create();
return [
'asset' => Asset::factory()->doesNotRequireAcceptance()->create(),
'asset' => $asset,
'acceptance' => null,
'first_time_sending' => true,
'expected_subject' => 'Asset checked out',
'expected_subject' => trans('mail.Asset_Checkout_Notification', ['tag' => $asset->asset_tag]),
'expected_opening' => 'A new item has been checked out under your name, details are below.'
];
}

View File

@@ -31,8 +31,8 @@ class NotificationTest extends TestCase
Mail::fake();
$asset->checkOut($user, $admin->id);
Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) use ($user) {
return $mail->hasTo($user->email) && $mail->hasSubject(trans('mail.Asset_Checkout_Notification'));
Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) use ($user, $asset) {
return $mail->hasTo($user->email) && $mail->hasSubject(trans('mail.Asset_Checkout_Notification', ['tag' => $asset->asset_tag]));
});
}
public function testDefaultEulaIsSentWhenSetInCategory()

View File

@@ -18,12 +18,14 @@ class SnipeModelTest extends TestCase
public function testSetsPurchaseCostsAppropriately()
{
$c = new SnipeModel;
$c->purchase_cost = '';
$this->assertTrue($c->purchase_cost == null);
$c->purchase_cost = '0.00';
$this->assertTrue($c->purchase_cost === null);
$this->assertTrue($c->purchase_cost == 0.00);
$c->purchase_cost = '9.54';
$this->assertTrue($c->purchase_cost === 9.54);
$this->assertTrue($c->purchase_cost == 9.54);
$c->purchase_cost = '9.50';
$this->assertTrue($c->purchase_cost === 9.5);
$this->assertTrue($c->purchase_cost == 9.5);
}
public function testNullsBlankLocationIdsButNotOthers()