Compare commits

...

92 Commits

Author SHA1 Message Date
snipe 685bbf4375 Ugh, this is awful
Signed-off-by: snipe <snipe@snipe.net>
2025-09-07 08:35:23 +01:00
snipe 5d03038734 Use auto-direction for <p> in preview
Signed-off-by: snipe <snipe@snipe.net>
2025-09-05 15:12:38 +01:00
snipe 75b11de0f4 Bumped composer packages
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 19:55:30 +01:00
snipe c5bede8594 More small display_name fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 17:15:06 +01:00
snipe cd9ea6ae3b Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:56:43 +01:00
snipe 113b762ec7 Fix for null location in locations print
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:55:44 +01:00
snipe 78704d8b85 Fixed #17803 - checked out to name in custom report
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:51:10 +01:00
snipe 1109db76fe Merge pull request #17797 from grokability/#17796-search-on-model-name-and-number
Fixed #17796 - search on model name and number on importer
2025-09-04 16:35:43 +01:00
snipe b1b390febf Removed flaky test
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:30:02 +01:00
snipe be451fa0c0 Removed asset model from original factory
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:14:27 +01:00
snipe 1fa553c785 Use nths
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:04:16 +01:00
snipe 905f61371d Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:02:40 +01:00
snipe 7da5210a01 Switched to nth from fetchOne in CSV reader
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 12:50:12 +01:00
snipe 18172d3896 Merge pull request #17752 from marcusmoore/fixes/existing-assets-report
Improve expiring alerts notification layout
2025-09-04 12:33:32 +01:00
snipe c28e78b9e2 Merge pull request #16063 from Godmartinz/checkin_non_reassignable_license
Allows check-ins of unreassignable licenses
2025-09-04 11:22:17 +01:00
snipe e7827a3847 Merge pull request #17800 from marcusmoore/chore/test-action-logs
Upload log file in GitHub Action tests
2025-09-04 10:20:12 +01:00
Godfrey M db9f85e9da Merge branch 'develop' into checkin_non_reassignable_license
# Conflicts:
#	app/Models/License.php
#	resources/views/licenses/view.blade.php
#	tests/Feature/Checkins/Api/LicenseCheckInTest.php
2025-09-03 11:09:51 -07:00
Marcus Moore 27022954b1 Inline icon 2025-09-03 10:44:19 -07:00
Marcus Moore 30362c924f Upload log as artifact 2025-09-03 10:13:01 -07:00
snipe bf63b15b46 Merge pull request #17799 from grokability/#17798-adds-require-serial-to-importer
Fixed #17798 - added `require_serial` to model importer
2025-09-03 15:32:42 +01:00
snipe 19aea4bd6c Fixed #17798 - added require_serial to model importer
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:25:56 +01:00
snipe 090890e9c6 Fixed #17796 - search on model name and number on importer
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:15:29 +01:00
snipe 605022a9e3 Merge pull request #17795 from grokability/#17791-larger-currency-field
Fixed #17791 - increase size of purchase cost field
2025-09-03 15:09:28 +01:00
snipe b06c58fe7b Switch to older style rules for consistency
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:06:27 +01:00
snipe f5c8b3eb04 Fixed #17791 - increase size of purchase cost field
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:03:49 +01:00
snipe 739980aa09 Merge pull request #16947 from Godmartinz/add-require-serial-to-models
Adds require serial as Asset Model option
2025-09-03 14:53:15 +01:00
snipe afde5943e3 Fixed typo - #17784
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 14:39:59 +01:00
snipe 32300cb42c Merge pull request #17788 from grokability/add-delete-log-instead-of-soft-deleting-the-log-itself
Fixed #17777 - Log upload deletion
2025-09-03 14:37:32 +01:00
snipe de3b1697c8 Merge pull request #17760 from Godmartinz/fix-translation-string-in-notifications
Fixes #17759 translation used in asset check in/out notifications
2025-09-03 14:33:57 +01:00
snipe a18fb10b5a Merge pull request #17783 from marcusmoore/fixes/company-in-location-print
Fixed company name reference in location print
2025-09-03 08:44:40 +01:00
snipe 52140dbe06 Log upload deletion
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 08:37:42 +01:00
Marcus Moore db5bb1928e Merge branch 'develop' into fixes/existing-assets-report
# Conflicts:
#	resources/views/notifications/markdown/report-expiring-assets.blade.php
2025-09-02 15:07:23 -07:00
Marcus Moore 65b66beb07 Make icon more prominent 2025-09-02 14:55:46 -07:00
Marcus Moore c83504b4e7 Use display_name in place of presenter 2025-09-02 12:57:40 -07:00
Godfrey M cd2e7ee31d fix google and slack notifications 2025-09-02 10:53:42 -07:00
Godfrey M c3a0a0415a fix MS Teams Notifications 2025-09-02 10:48:02 -07:00
snipe 709f4672b7 Merge pull request #17771 from grokability/#10107-remember-checkout-to-type
Fixed #10107 - remember checkout to type
2025-09-02 13:46:13 +01:00
snipe e6c030b050 Merge pull request #17781 from grokability/#17780-add-withtrashed-to-files
Fixed #17780 - Added `withTrashed()` to allow viewing files on deleted objects
2025-09-02 13:45:18 +01:00
snipe 7bd3a791a1 Added withTrashed() to allow viewing files on deleted objects
Signed-off-by: snipe <snipe@snipe.net>
2025-09-02 13:39:35 +01:00
snipe b9cfc03b4f display name fix
Signed-off-by: snipe <snipe@snipe.net>
2025-09-02 11:30:34 +01:00
snipe 131327a64d Fixed “undefined” error on status labels in BS tables
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 20:32:42 +01:00
snipe 77d002a158 Use null for role as well
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:43:05 +01:00
snipe 94699893ac Updated fieldname in user API request
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:17:39 +01:00
snipe 9f81989bdd Return null instead of blank for display_name in API
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:06:28 +01:00
snipe 15abe36c53 More tweaks to display
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:58:02 +01:00
snipe 3094e007ee Set session to remember checkout type
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:28:07 +01:00
snipe eb259aee22 Set asset selector to true for components since it will always be required
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:18:58 +01:00
snipe c05c8defb9 Merge pull request #17769 from grokability/#9978-add-pivot-to-accessories-api
Fixes #9978 - Added payload to accessories API
2025-09-01 12:28:08 +01:00
snipe bf5668a42e Added payload to accessories API
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 12:23:39 +01:00
Godfrey M eadce51f10 fixes check in check out translations for assets in notiications 2025-08-29 11:12:05 -07:00
Marcus Moore 35b79e4d14 Remove old comments 2025-08-28 16:56:40 -07:00
Marcus Moore 751dad7f2e Inline days 2025-08-28 16:56:12 -07:00
Marcus Moore b08d86220a First pass at moving to table structure 2025-08-28 16:53:13 -07:00
Godfrey M 2b8ea9a233 add required to input validation 2025-07-07 10:46:54 -07:00
Godfrey M b0067fee51 remove some N+1s, collect an array of missing serial errors 2025-05-20 12:31:34 -07:00
Godfrey M 732c3dae89 added require_serial to model factory 2025-05-20 09:53:51 -07:00
Godfrey M d45bd67cae added corrections 2025-05-20 09:51:02 -07:00
Godfrey M 9200de5032 made require_serial column nullable 2025-05-19 09:58:37 -07:00
Godfrey M 3fbbff5a47 revert unnecessary change to laels 2025-05-14 12:57:01 -07:00
Godfrey M c22efc2c3d add to present and transformer and api 2025-05-14 12:55:41 -07:00
Godfrey M 8c0281bf70 adds tests for requiring serial to asset model 2025-05-14 12:31:27 -07:00
Godfrey M 720a4bc4a2 add warning to update method for missing a serial 2025-05-13 12:54:28 -07:00
Godfrey M 7fd93645b3 valdiation fires for asset creation 2025-05-13 12:17:58 -07:00
Godfrey M fcbfbca6d0 add checkbox to model edit and create 2025-05-13 11:10:46 -07:00
Godfrey M f2bca9491c changed name of field in model fillable 2025-05-13 10:59:02 -07:00
Godfrey M b48f309ab6 add require_serial to bulk asset model blades and lang 2025-05-13 10:58:05 -07:00
Godfrey M 0b1be3e63b add migration, model and controller update 2025-05-12 11:44:34 -07:00
Godfrey M 8c129c10af removed payload from api error message 2025-04-30 11:11:53 -07:00
Godfrey M 63ce2a14fe fix the ghosts pt2 2025-04-23 10:17:11 -07:00
Godfrey M f435ebb110 fix the ghosts 2025-04-23 10:15:26 -07:00
Godfrey M 843f001bf6 rename test class 2025-04-23 09:33:56 -07:00
Godfrey M 0d28165c04 merged develop, fix conflicts 2025-04-21 10:54:01 -07:00
Godfrey M ee31bfbcd4 Merge branch 'develop' into checkin_non_reassignable_license
# Conflicts:
#	app/Helpers/Helper.php
#	app/Http/Controllers/Api/LicenseSeatsController.php
#	app/Http/Transformers/LicensesTransformer.php
2025-04-21 10:49:35 -07:00
Godfrey M 1c67d6802d added testing to Api check in, renamed other test method 2025-01-27 12:18:24 -08:00
Godfrey M 5da8c86ec7 fix license query for bulk licenses checkin for assets 2025-01-22 10:55:41 -08:00
Godfrey Martinez 10a2d59ec1 Merge pull request #27 from Godmartinz/checkin_non_reassignable_license_cleanup
adds bulk check in of unreassinable licenses, cleans up methods used for counting.
2025-01-22 10:49:47 -08:00
Godfrey M 34e8360b10 moves counts to licenses, allows bulk check in of unreassignable licenses 2025-01-22 10:46:28 -08:00
Godfrey Martinez ca259ee4c3 Merge pull request #26 from Godmartinz/checkin_non_reassignable_license_cleanup
moved methods to licenseSeat model, clean up code, removed unused namespaces etc
2025-01-21 11:47:42 -08:00
Godfrey M 2143952a1e removed unused models, castged unreassignable_seat, clean up in general 2025-01-21 11:43:47 -08:00
Godfrey M 6bab6e7151 remove unintended change 2025-01-16 12:27:01 -08:00
Godfrey M d217c2e295 last rename 2025-01-16 12:24:32 -08:00
Godfrey M 52bf0faaa5 renamed unassignable to reassignable_seat 2025-01-16 12:19:59 -08:00
Godfrey M 3f3f2bfc61 fixed factory and test 2025-01-16 12:14:03 -08:00
Godfrey M f050864fb4 renamed dead to unassigned, replaced 0 and 1 with true and flase 2025-01-16 12:12:14 -08:00
Godfrey M db11fc35f4 fixed typo, fixed variable name 2025-01-16 12:00:49 -08:00
Godfrey M f47a2b10c0 updated column name, updated Api license checkin and out 2025-01-16 11:53:15 -08:00
Godfrey M 344b4e7d60 fixes test to check if checked in licenses is unavailable 2025-01-16 09:46:03 -08:00
Godfrey M 7a23372489 adds migration for column unavailable, changes logic to utlize value 2025-01-15 15:36:12 -08:00
Godfrey M 9da15a8e58 get button color correct 2025-01-13 14:44:26 -08:00
Godfrey M 50e0e4a07b remove unrelated change 2025-01-13 14:37:03 -08:00
Godfrey M 5e1562ae4c adds count on tabs and reports and index 2025-01-13 14:31:54 -08:00
Godfrey M 8a0ed49623 allows checkin of unreassignable license seats 2025-01-13 13:35:48 -08:00
83 changed files with 1254 additions and 918 deletions
+12
View File
@@ -76,4 +76,16 @@ jobs:
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7
+12
View File
@@ -75,4 +75,16 @@ jobs:
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7
+12
View File
@@ -61,4 +61,16 @@ jobs:
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7
+8 -1
View File
@@ -82,6 +82,13 @@ class Helper
'zu' => 'zu-ZA', // Zulu
];
public static function hasRtl($value) {
$rtlChar = '/[\x{0590}-\x{083F}]|[\x{08A0}-\x{08FF}]|[\x{FB1D}-\x{FDFF}]|[\x{FE70}-\x{FEFF}]/u';
return preg_match($rtlChar, $value) != 0;
}
/**
* Simple helper to invoke the markdown parser
*
@@ -1706,5 +1713,5 @@ class Helper
}
}
return $mismatched;
}
}
}
+1
View File
@@ -16,6 +16,7 @@ class IconHelper
case 'clone':
return 'far fa-clone';
case 'delete':
case 'upload deleted':
return 'fas fa-trash';
case 'create':
return 'fa-solid fa-plus';
@@ -71,6 +71,7 @@ class AccessoryCheckoutController extends Controller
$this->authorize('checkout', $accessory);
$target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
$accessory->checkout_qty = $request->input('checkout_qty', 1);
@@ -30,11 +30,12 @@ use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use TCPDF;
use App\Helpers\Helper;
class AcceptanceController extends Controller
{
@@ -178,7 +179,7 @@ class AcceptanceController extends Controller
break;
case 'App\Models\Component':
$pdf_view_route ='account.accept.accept-component-eula';
$pdf_view_route = 'account.accept.accept-component-eula';
$component = Component::find($item->id);
$display_model = $component->name;
break;
@@ -217,7 +218,7 @@ class AcceptanceController extends Controller
} elseif (!is_null($branding_settings->logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->logo;
}
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $display_model,
@@ -225,9 +226,9 @@ class AcceptanceController extends Controller
'item_status' => $item->assetstatus?->name,
'eula' => $item->getEula(),
'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
'assigned_to' => $assigned_user->present()->fullName,
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d H:i:s'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d H:i:s'),
'assigned_to' => $assigned_user->display_name,
'company_name' => $branding_settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
@@ -235,13 +236,88 @@ class AcceptanceController extends Controller
'admin' => auth()->user()->present()?->fullName,
];
if ($pdf_view_route!='') {
Log::debug($pdf_filename.' is the filename, and the route was specified.');
$pdf = Pdf::loadView($pdf_view_route, $data);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
//$pdf = new PDF;
// set some language dependent data:
$lg = Array();
$lg['a_meta_charset'] = 'UTF-8';
$lg['w_page'] = 'page';
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
// $pdf->SetHeaderData(PDF_HEADER_LOGO, 5, PDF_HEADER_TITLE.' 006', PDF_HEADER_STRING);
// $pdf->SetHeaderData('https://snipe-it.test/uploads/snipe-logo.png', '5', $data['company_name'], $item->company?->name);
//$pdf->headerText = ('Anything you want ' . date('c'));
$pdf->setRTL(false);
//$pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, $data['company_name'], '');
$pdf->setLanguageArray($lg);
$pdf->SetCreator('Snipe-IT');
$pdf->SetAuthor($data['assigned_to']);
$pdf->SetTitle('Asset Acceptance: '.$data['item_tag']);
// $pdf->SetSubject('Document Subject');
//$pdf->SetKeywords('keywords, here');
$pdf->SetFont('dejavusans', '', 8, '', true);
$pdf->SetPrintHeader(false);
$pdf->SetPrintFooter(false);
$pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN));
$pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA));
$pdf->AddPage();
$pdf->writeHTML('<img src="'.$path_logo.'" height="30">', true, 0, true, 0, '');
// $pdf->writeHTML(trans('general.date').': '.date($data['date_settings']), true, 0, true, 0, '');
$pdf->writeHTML("<strong>".trans('general.asset_tag').'</strong>: '.$data['item_tag'], true, 0, true, 0, '');
$pdf->writeHTML("<strong>".trans('general.asset_model').'</strong>: '.$data['item_model'], true, 0, true, 0, '');
$pdf->writeHTML("<strong>".trans('admin/hardware/form.serial').'</strong>: '.$data['item_serial'], true, 0, true, 0, '');
$pdf->writeHTML("<strong>".trans('general.assigned_date').'</strong>: '.$data['check_out_date'], true, 0, true, 0, '');
$pdf->writeHTML("<strong>".trans('general.assignee').'</strong>: '.$data['assigned_to'], true, 0, true, 0, '');
$pdf->Ln();
// $html = view($pdf_view_route, $data)->render();
// $pdf->writeHTML($html, true, 0, true, 0, '');
// $eula_lines = explode("\n\n", $item->getEula());
$eula_lines = preg_split("/\r\n|\n|\r/", $item->getEula());
foreach ($eula_lines as $eula_line) {
if (Helper::hasRtl($eula_line)) {
$pdf->setRTL(true);
} else {
$pdf->setRTL(false);
}
$pdf->writeHTML(Helper::parseEscapedMarkedown($eula_line), true, 0, true, 0, '');
}
$pdf->Ln();
$pdf->Ln();
$pdf->setRTL(false);
$pdf->writeHTML('<br><br>', true, 0, true, 0, '');
if ($data['note'] != null) {
$pdf->writeHTML("<strong>".trans('general.notes') . '</strong>: ' . $data['note'], true, 0, true, 0, '');
$pdf->Ln();
}
if ($data['signature'] != null) {
$pdf->writeHTML('<img src="'.$data['signature'].'" style="max-width: 600px;">', true, 0, true, 0, '');
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
}
$pdf->writeHTML("<strong>".trans('general.accepted_date').'</strong>: '.$data['accepted_date'], true, 0, true, 0, '');
$pdf_content = $pdf->Output($pdf_filename, 'S');
//$html = view($pdf_view_route, $data)->render();
//$pdf = PDF::writeHTML($html, true, false, true, false, '');
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf_content);
}
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
// $acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
// Send the PDF to the signing user
if (($request->input('send_copy') == '1') && ($assigned_user->email !='')) {
@@ -288,32 +288,42 @@ class AccessoriesController extends Controller
'note' => $request->input('note'),
]);
$accessory_checkout->created_by = auth()->id();
$accessory_checkout->save();
$payload = [
'accessory_id' => $accessory->id,
'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'),
'created_by' => auth()->id(),
'pivot' => $accessory_checkout->id,
];
}
// Set this value to be able to pass the qty through to the event
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
}
/**
* Check in the item so that it can be checked out again to someone else
*
* @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request
* @param int $accessoryUserId
* @param string $backto
* @return \Illuminate\Http\RedirectResponse
* @return JsonResponse
* @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>]
* @internal param int $accessoryId
*/
public function checkin(Request $request, $accessoryUserId = null)
{
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist', ['id' => $accessoryUserId])));
}
$accessory = Accessory::find($accessory_checkout->accessory_id);
@@ -327,7 +337,14 @@ class AccessoriesController extends Controller
$user = User::find($accessory_checkout->assigned_to);
}
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkin.success')));
$payload = [
'accessory_id' => $accessory->id,
'note' => $request->input('note'),
'created_by' => auth()->id(),
'pivot' => $accessory_checkout->id,
];
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkin.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkin.error')));
@@ -50,6 +50,7 @@ class AssetModelsController extends Controller
'fieldset',
'deleted_at',
'updated_at',
'require_serial',
];
$assetmodels = AssetModel::select([
@@ -69,6 +70,7 @@ class AssetModelsController extends Controller
'models.fieldset_id',
'models.deleted_at',
'models.updated_at',
'models.require_serial'
])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
->withCount('assets as assets_count');
@@ -69,7 +69,7 @@ class ImportController extends Controller
if (function_exists('iconv')) {
$file_contents = $file->getContent(); //TODO - this *does* load the whole file in RAM, but we need that to be able to 'iconv' it?
$encoding = $detector->getEncoding($file_contents);
\Log::warning("Discovered encoding: $encoding in uploaded CSV");
\Log::debug("Discovered encoding: $encoding in uploaded CSV");
$reader = null;
if (strcasecmp($encoding, 'UTF-8') != 0) {
$transliterated = false;
@@ -103,7 +103,7 @@ class ImportController extends Controller
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
try {
$import->header_row = $reader->fetchOne(0);
$import->header_row = $reader->nth(0);
} catch (JsonEncodingException $e) {
return response()->json(
Helper::formatStandardApiResponse(
@@ -136,7 +136,7 @@ class ImportController extends Controller
try {
// Grab the first row to display via ajax as the user picks fields
$import->first_row = $reader->fetchOne(1);
$import->first_row = $reader->nth(1);
} catch (JsonEncodingException $e) {
return response()->json(
Helper::formatStandardApiResponse(
@@ -128,7 +128,9 @@ class LicenseSeatsController extends Controller
// nothing to update
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
if( $touched && $licenseSeat->unreassignable_seat) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
}
// the logging functions expect only one "target". if both asset and user are present in the request,
// we simply let assets take precedence over users...
if ($licenseSeat->isDirty('assigned_to')) {
@@ -145,7 +147,11 @@ class LicenseSeatsController extends Controller
if ($licenseSeat->save()) {
if ($is_checkin) {
$licenseSeat->logCheckin($target, $request->input('notes'));
if(!$licenseSeat->license->reassignable){
$licenseSeat->unreassignable_seat = true;
$licenseSeat->save();
}
$licenseSeat->logCheckin($target, $licenseSeat->notes);
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
@@ -69,7 +69,7 @@ class ProfileController extends Controller
if ($checkoutRequest && $checkoutRequest->itemRequested()) {
$assets = [
'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()),
'name' => e($checkoutRequest->itemRequested()->present()->name()),
'name' => e($checkoutRequest->itemRequested()->display_name),
'type' => e($checkoutRequest->itemType()),
'qty' => (int) $checkoutRequest->quantity,
'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null,
@@ -32,7 +32,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object);
if (!$object) {
@@ -51,11 +51,7 @@ class UploadedFilesController extends Controller
];
$uploads = Actionlog::select('action_logs.*')
->whereNotNull('filename')
->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)
->where('action_type', '=', 'uploaded')
$uploads = self::$map_object_type[$object_type]::withTrashed()->find($id)->uploads()
->with('adminuser');
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
@@ -96,7 +92,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object);
if (!$object) {
@@ -144,7 +140,7 @@ class UploadedFilesController extends Controller
public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object);
if (!$object) {
@@ -188,7 +184,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
@@ -206,7 +202,7 @@ class UploadedFilesController extends Controller
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
if ($log->logUploadDelete($object, $log->filename)) {
return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
}
+2 -2
View File
@@ -435,7 +435,7 @@ class UsersController extends Controller
$user->password = $user->noPassword();
}
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) {
@@ -560,7 +560,7 @@ class UsersController extends Controller
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
}
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) {
// Check if the request has groups passed and has a value, AND that the user us a superuser
@@ -82,6 +82,7 @@ class AssetModelsController extends Controller
$model->notes = $request->input('notes');
$model->created_by = auth()->id();
$model->requestable = $request->has('requestable');
$model->require_serial = $request->input('require_serial', 0);
if ($request->input('fieldset_id') != '') {
$model->fieldset_id = $request->input('fieldset_id');
@@ -155,7 +156,7 @@ class AssetModelsController extends Controller
$model->category_id = $request->input('category_id');
$model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0');
$model->require_serial = $request->input('require_serial', 0);
$model->fieldset_id = $request->input('fieldset_id');
if ($model->save()) {
@@ -65,6 +65,8 @@ class AssetCheckoutController extends Controller
*/
public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse
{
try {
// Check if the asset exists
if (! $asset = Asset::find($assetId)) {
@@ -81,6 +83,7 @@ class AssetCheckoutController extends Controller
$admin = auth()->user();
$target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
$asset = $this->updateAssetLocation($asset, $target);
@@ -110,17 +110,35 @@ class AssetsController extends Controller
// This is only necessary on create, not update, since bulk editing is handled
// differently
$asset_tags = $request->input('asset_tags');
$model = AssetModel::find($request->input('model_id'));
$serial_errors = [];
$serials = $request->input('serials');
$settings = Setting::getSettings();
//Validate required serial based on model setting
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
if ($model && $model->require_serial === 1 && empty($serials[$a])) {
$serial_errors["serials.$a"] = trans('admin/hardware/form.serial_required', ['number' => $a]);
}
}
if (!empty($serial_errors)) {
return redirect()->back()
->withInput()
->withErrors($serial_errors);
}
$asset = null;
$companyId = Company::getIdForCurrentUser($request->input('company_id'));
$successes = [];
$failures = [];
$serials = $request->input('serials');
$asset = null;
for ($a = 1; $a <= count($asset_tags); $a++) {
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
$asset = new Asset();
$asset->model()->associate(AssetModel::find($request->input('model_id')));
$asset->model()->associate($model);
$asset->name = $request->input('name');
// Check for a corresponding serial
@@ -132,7 +150,7 @@ class AssetsController extends Controller
$asset->asset_tag = $asset_tags[$a];
}
$asset->company_id = Company::getIdForCurrentUser($request->input('company_id'));
$asset->company_id = $companyId;
$asset->model_id = $request->input('model_id');
$asset->order_number = $request->input('order_number');
$asset->notes = $request->input('notes');
@@ -172,7 +190,6 @@ class AssetsController extends Controller
// Update custom fields in the database.
// Validation for these fields is handled through the AssetRequest form request
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
@@ -453,6 +470,13 @@ class AssetsController extends Controller
]);
//Validate required serial based on model setting
if ($model && $model->require_serial === 1 && empty($serial[1])) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
'asset_model' => $model->name
]));
}
if ($asset->save()) {
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.update.success'));
@@ -637,6 +637,7 @@ class BulkAssetsController extends Controller
$admin = auth()->user();
$target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
if (! is_array($request->get('selected_assets'))) {
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
@@ -92,7 +92,9 @@ class BulkAssetModelsController extends Controller
$update_array['min_amt'] = $request->input('min_amt');
}
if ($request->filled('require_serial')) {
$update_array['require_serial'] = $request->input('require_serial');
}
if (count($update_array) > 0) {
AssetModel::whereIn('id', $models_raw_array)->update($update_array);
@@ -64,12 +64,7 @@ class LicenseCheckinController extends Controller
$this->authorize('checkout', $license);
if (! $license->reassignable) {
// Not allowed to checkin
Session::flash('error', trans('admin/licenses/message.checkin.not_reassignable') . '.');
return redirect()->back()->withInput();
}
// Declare the rules for the form validation
$rules = [
@@ -98,6 +93,9 @@ class LicenseCheckinController extends Controller
$licenseSeat->assigned_to = null;
$licenseSeat->asset_id = null;
$licenseSeat->notes = $request->input('notes');
if (! $licenseSeat->license->reassignable) {
$licenseSeat->unreassignable_seat = true;
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($request->get('redirect_option') === 'target'){
@@ -106,7 +104,7 @@ class LicenseCheckinController extends Controller
// Was the asset updated?
if ($licenseSeat->save()) {
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes')));
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $licenseSeat->notes));
return Helper::getRedirectOption($request, $license->id, 'Licenses')
@@ -132,21 +130,17 @@ class LicenseCheckinController extends Controller
$license = License::findOrFail($licenseId);
$this->authorize('checkin', $license);
if (! $license->reassignable) {
// Not allowed to checkin
Session::flash('error', 'License not reassignable.');
return redirect()->back()->withInput();
}
$licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId)
->whereNotNull('assigned_to')
->with('user')
->with('user', 'license')
->get();
$license = $licenseSeatsByUser->first()?->license;
foreach ($licenseSeatsByUser as $user_seat) {
$user_seat->assigned_to = null;
if ($license && ! $license->reassignable) {
$user_seat->unreassignable_seat = true;
}
if ($user_seat->save()) {
Log::debug('Checking in '.$license->name.' from user '.$user_seat->username);
$user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
@@ -159,9 +153,12 @@ class LicenseCheckinController extends Controller
->get();
$count = 0;
$license = $licenseSeatsByAsset->first()?->license;
foreach ($licenseSeatsByAsset as $asset_seat) {
$asset_seat->asset_id = null;
if ($license && ! $license->reassignable) {
$asset_seat->unreassignable_seat = true;
}
if ($asset_seat->save()) {
Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag);
$asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
@@ -39,6 +39,11 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
// We don't currently allow checking out licenses to locations, so we'll reset that to user if needed
if (session()->get('checkout_to_type') == 'location') {
session()->put(['checkout_to_type' => 'user']);
}
// Return the checkout view
return view('licenses/checkout', compact('license'));
}
@@ -70,17 +75,15 @@ class LicenseCheckoutController extends Controller
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
$licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes');
$checkoutMethod = 'checkoutTo'.ucwords(request('checkout_to_type'));
if ($request->filled('asset_id')) {
session()->put(['checkout_to_type' => 'asset']);
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
$request->request->add(['assigned_asset' => $checkoutTarget->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'asset']);
} elseif ($request->filled('assigned_to')) {
session()->put(['checkout_to_type' => 'user']);
$checkoutTarget = $this->checkoutToUser($licenseSeat);
$request->request->add(['assigned_user' => $checkoutTarget->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'user']);
@@ -89,6 +92,7 @@ class LicenseCheckoutController extends Controller
if ($checkoutTarget) {
return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.checkout.success'));
}
@@ -245,16 +245,25 @@ class LicensesController extends Controller
$license = License::with('assignedusers')->find($license->id);
$users_count = User::where('autoassign_licenses', '1')->count();
$total_seats_count = $license->totalSeatsByLicenseID();
$total_seats_count = (int) $license->totalSeatsByLicenseID();
$available_seats_count = $license->availCount()->count();
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
$unreassignable_seats_count = License::unReassignableCount($license);
if(!$license->reassignable){
$checkedout_seats_count = ($total_seats_count - $available_seats_count - $unreassignable_seats_count );
}
else {
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
}
$this->authorize('view', $license);
return view('licenses.view', compact('license'))
->with('users_count', $users_count)
->with('total_seats_count', $total_seats_count)
->with('available_seats_count', $available_seats_count)
->with('checkedout_seats_count', $checkedout_seats_count);
->with('checkedout_seats_count', $checkedout_seats_count)
->with('unreassignable_seats_count', $unreassignable_seats_count);
}
+4 -4
View File
@@ -159,7 +159,7 @@ class ReportsController extends Controller
$row[] = e($asset->serial);
if ($target = $asset->assignedTo) {
$row[] = e($target->present()->name());
$row[] = e($target->display_name);
} else {
$row[] = ''; // Empty string if unassigned
}
@@ -856,8 +856,8 @@ class ReportsController extends Controller
}
if ($request->filled('assigned_to')) {
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ?? $asset->assigned->display_name;
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? 'user' : $asset->assignedType();
$row[] = ($asset->assigned) ? $asset->assigned->display_name : '';
$row[] = ($asset->assigned) ? $asset->assignedType() : '';
}
if ($request->filled('username')) {
@@ -1260,7 +1260,7 @@ class ReportsController extends Controller
$row[] = str_replace(',', '', e($item['assetItem']->model->name));
$row[] = str_replace(',', '', e($item['assetItem']->name));
$row[] = str_replace(',', '', e($item['assetItem']->asset_tag));
$row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->present()->name() : trans('admin/reports/general.deleted_user')));
$row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->display_name : trans('admin/reports/general.deleted_user')));
$rows[] = implode(',', $row);
}
}
@@ -36,7 +36,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', $object);
if (!$object) {
@@ -85,7 +85,7 @@ class UploadedFilesController extends Controller
public function show($object_type, $id, $file_id) : RedirectResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object);
if (!$object) {
@@ -130,7 +130,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
@@ -148,7 +148,7 @@ class UploadedFilesController extends Controller
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
if ($log->logUploadDelete($object, $log->filename)) {
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.success', 1));
}
@@ -50,17 +50,20 @@ class ActionlogsTransformer
public function transformActionlog (Actionlog $actionlog, $settings = null)
{
$icon = $actionlog->present()->icon();
if (($actionlog->filename!='') && ($actionlog->action_type!='upload deleted')) {
$icon = Helper::filetype_icon($actionlog->filename);
}
static $custom_fields = false;
if ($custom_fields === false) {
$custom_fields = CustomField::all();
}
if ($actionlog->filename!='') {
$icon = Helper::filetype_icon($actionlog->filename);
}
// This is necessary since we can't escape special characters within a JSON object
if (($actionlog->log_meta) && ($actionlog->log_meta!='')) {
@@ -65,6 +65,7 @@ class AssetModelsTransformer
'default_fieldset_values' => $default_field_values,
'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None',
'requestable' => ($assetmodel->requestable == '1') ? true : false,
'require_serial' => $assetmodel->require_serial,
'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes),
'created_by' => ($assetmodel->adminuser) ? [
'id' => (int) $assetmodel->adminuser->id,
@@ -7,7 +7,6 @@ use App\Models\License;
use App\Models\LicenseSeat;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
class LicenseSeatsTransformer
{
public function transformLicenseSeats(Collection $seats, $total)
@@ -52,6 +51,7 @@ class LicenseSeatsTransformer
'reassignable' => (bool) $seat->license->reassignable,
'notes' => e($seat->notes),
'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')),
'disabled' => $seat->unreassignable_seat,
];
$permissions_array['available_actions'] = [
@@ -37,7 +37,7 @@ class LicensesTransformer
'notes' => Helper::parseEscapedMarkedownInline($license->notes),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'seats' => (int) $license->seats,
'free_seats_count' => (int) $license->free_seats_count,
'free_seats_count' => (int) $license->free_seats_count - License::unReassignableCount($license),
'remaining' => (int) $license->free_seats_count,
'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null,
'license_name' => ($license->license_name) ? e($license->license_name) : null,
+1 -1
View File
@@ -25,7 +25,7 @@ class ProfileTransformer
'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename),
'item' => ($file->item) ? [
'name' => ($file->itemType()=='user') ? e($file->item->display_name) : e($file->item->getDisplayNameAttribute()),
'name' => $file->item->display_name ? e($file->item->display_name) : null,
'type' => e($file->itemType()),
] : null,
'filename' => e($file->filename),
+2 -2
View File
@@ -22,7 +22,7 @@ class UsersTransformer
public function transformUser(User $user)
{
$role = '';
$role = null;
if ($user->isSuperUser()) {
$role = 'superadmin';
} elseif ($user->isAdmin()) {
@@ -34,7 +34,7 @@ class UsersTransformer
'name' => e($user->getFullNameAttribute()) ?? null,
'first_name' => e($user->first_name) ?? null,
'last_name' => e($user->last_name) ?? null,
'display_name' => e($user->getRawOriginal('display_name')) ?? null,
'display_name' => ($user->getRawOriginal('display_name')) ? e($user->getRawOriginal('display_name')) : null,
'username' => e($user->username) ?? null,
'remote' => ($user->remote == '1') ? true : false,
'locale' => ($user->locale) ? e($user->locale) : null,
+24 -2
View File
@@ -40,11 +40,32 @@ class AssetModelImporter extends ItemImporter
{
$editingAssetModel = false;
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
/**
* This part gets a little confusing, since folks might be importing multiple models with the same name and different model numbers for the first time
* or they might be wanting to update existing models with new model numbers.
*/
// They are not trying to update existing models, so we'll check for duplicates with model name *and* number
if (! $this->updating) {
$this->log('Finding model by name and model number: '.$this->findCsvMatch($row, 'name').' / '.$this->findCsvMatch($row, 'model_number'));
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->where('model_number', '=', $this->findCsvMatch($row, 'model_number'))->first();
} else {
if ($this->findCsvMatch($row, 'id')!='') {
// Override model if an ID was given
$this->log('Finding model by ID: '.$this->findCsvMatch($row, 'id'));
$assetModel = AssetModel::find($this->findCsvMatch($row, 'id'));
} else {
$this->log('Finding model by name: '.$this->findCsvMatch($row, 'name'));
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
}
}
if ($assetModel) {
if (! $this->updating) {
$this->log('A matching Model '.$this->item['name'].' already exists');
$this->log('A matching Model '.$this->item['name'].' already exists and we are not updating. Skipping.');
return;
}
@@ -66,6 +87,7 @@ class AssetModelImporter extends ItemImporter
$this->item['fieldset'] = trim($this->findCsvMatch($row, 'fieldset'));
$this->item['depreciation'] = trim($this->findCsvMatch($row, 'depreciation'));
$this->item['requestable'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'requestable'))) == 1) ? 1 : 0;
$this->item['require_serial'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'require_serial'))) == 1) ? 1 : 0;
if (!empty($this->item['category'])) {
if ($category = $this->createOrFetchCategory($this->item['category'])) {
+6
View File
@@ -403,6 +403,7 @@ class Importer extends Component
$this->assetmodels_fields = [
'id' => trans('general.id'),
'category' => trans('general.category'),
'eol' => trans('general.eol'),
'fieldset' => trans('admin/models/general.fieldset'),
@@ -412,6 +413,7 @@ class Importer extends Component
'model_number' => trans('general.model_no'),
'notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]),
'requestable' => trans('admin/models/general.requestable'),
'require_serial' => trans('admin/hardware/general.require_serial'),
];
@@ -535,6 +537,10 @@ class Importer extends Component
'product key',
'key',
],
'require_serial' =>
[
'serial required',
],
'model_number' =>
[
'model',
+1 -1
View File
@@ -66,7 +66,7 @@ class Accessory extends SnipeModel
'company_id' => 'integer|nullable',
'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|nullable',
'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',
'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'purchase_date' => 'date_format:Y-m-d|nullable',
];
+20 -13
View File
@@ -246,19 +246,6 @@ class Actionlog extends SnipeModel
}
/**
* Establishes the actionlog -> uploads relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
{
return $this->morphTo('item')
->where('action_type', '=', 'uploaded')
->withTrashed();
}
/**
* Establishes the actionlog -> userlog relationship
@@ -456,6 +443,26 @@ class Actionlog extends SnipeModel
}
/**
* @author Godfrey Martinez
* @since [v8.0.4]
* @return \App\Models\Actionlog
*/
public function logUploadDelete($object, $filename)
{
$log = new Actionlog;
$log->item_type = $object instanceof SnipeModel ? get_class($object) : $object;
$log->item_id = $object->id;
$log->created_by = auth()->id();
$log->target_id = null;
$log->filename = $filename;
$log->created_at = date('Y-m-d H:i:s');
$log->logaction('upload deleted');
return $log;
}
public function uploads_file_url()
{
+3 -3
View File
@@ -114,7 +114,7 @@ class Asset extends Depreciable
'rtd_location_id' => ['nullable', 'exists:locations,id', 'fmcs_location'],
'purchase_date' => ['nullable', 'date', 'date_format:Y-m-d'],
'serial' => ['nullable', 'string', 'unique_undeleted:assets,serial'],
'purchase_cost' => ['nullable', 'numeric', 'gte:0', 'max:9999999999999'],
'purchase_cost' => ['nullable', 'numeric', 'gte:0', 'max:99999999999999999.99'],
'supplier_id' => ['nullable', 'exists:suppliers,id'],
'asset_eol_date' => ['nullable', 'date'],
'eol_explicit' => ['nullable', 'boolean'],
@@ -1033,9 +1033,9 @@ class Asset extends Depreciable
if (($this->model) && ($this->model->category)) {
if (($this->model->category->eula_text) && ($this->model->category->use_default_eula == 0)) {
return Helper::parseEscapedMarkedown($this->model->category->eula_text);
return $this->model->category->eula_text;
} elseif ($this->model->category->use_default_eula == 1) {
return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
return Setting::getSettings()->default_eula_text;
} else {
return false;
+1
View File
@@ -71,6 +71,7 @@ class AssetModel extends SnipeModel
'name',
'notes',
'requestable',
'require_serial'
];
use Searchable;
+1 -1
View File
@@ -46,7 +46,7 @@ class CheckoutRequest extends Model
public function name()
{
if ($this->itemType() == 'asset') {
return $this->itemRequested()->present()->name();
return $this->itemRequested()->display_name;
}
return $this->itemRequested()->name;
+1 -1
View File
@@ -43,7 +43,7 @@ class Component extends SnipeModel
'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|nullable',
'purchase_date' => 'date_format:Y-m-d|nullable',
'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',
'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'manufacturer_id' => 'integer|exists:manufacturers,id|nullable',
];
+1 -1
View File
@@ -47,7 +47,7 @@ class Consumable extends SnipeModel
'company_id' => 'integer|nullable',
'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|max:99999|nullable',
'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',
'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'purchase_date' => 'date_format:Y-m-d|nullable',
];
+26 -10
View File
@@ -51,7 +51,7 @@ class License extends Depreciable
'notes' => 'string|nullable',
'category_id' => 'required|exists:categories,id',
'company_id' => 'integer|nullable',
'purchase_cost'=> 'numeric|nullable|gte:0',
'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'purchase_date' => 'date_format:Y-m-d|nullable|max:10|required_with:depreciation_id',
'expiration_date' => 'date_format:Y-m-d|nullable|max:10',
'termination_date' => 'date_format:Y-m-d|nullable|max:10',
@@ -534,6 +534,7 @@ class License extends Depreciable
return $this->licenseSeatsRelation()
->whereNull('asset_id')
->whereNull('assigned_to')
->where('unreassignable_seat', '=', false)
->whereNull('deleted_at');
}
@@ -585,7 +586,22 @@ class License extends Depreciable
return 0;
}
/**
* Calculates the number of unreassignable seats
*
* @author G. Martinez
* @since [v7.1.15]
*/
public static function unReassignableCount($license) : int
{
$count = 0;
if (!$license->reassignable) {
$count = licenseSeat::query()->where('unreassignable_seat', '=', true)
->where('license_id', '=', $license->id)
->count();
}
return $count;
}
/**
* Calculates the number of remaining seats
*
@@ -593,11 +609,12 @@ class License extends Depreciable
* @since [v1.0]
* @return int
*/
public function remaincount()
public function remaincount() : int
{
$total = $this->licenseSeatsCount;
$taken = $this->assigned_seats_count;
$diff = ($total - $taken);
$unreassignable = self::unReassignableCount($this);
$diff = ($total - $taken - $unreassignable);
return (int) $diff;
}
@@ -655,12 +672,11 @@ class License extends Depreciable
{
return $this->licenseseats()
->whereNull('deleted_at')
->where(
function ($query) {
$query->whereNull('assigned_to')
->whereNull('asset_id');
}
)
->where('unreassignable_seat', '=', false)
->where(function ($query) {
$query->whereNull('assigned_to')
->whereNull('asset_id');
})
->orderBy('id', 'asc')
->first();
}
+3
View File
@@ -22,6 +22,9 @@ class LicenseSeat extends SnipeModel implements ICompanyableChild
protected $guarded = 'id';
protected $table = 'license_seats';
protected $casts = [
'unreassignable_seat' => 'boolean',
];
/**
* The attributes that are mass assignable.
+2 -2
View File
@@ -32,12 +32,12 @@ class Maintenance extends SnipeModel implements ICompanyableChild
'asset_id' => 'required|integer',
'supplier_id' => 'nullable|integer',
'asset_maintenance_type' => 'required',
'name' => 'required|max:100',
'name' => 'required|max:100',
'is_warranty' => 'boolean',
'start_date' => 'required|date_format:Y-m-d',
'completion_date' => 'date_format:Y-m-d|nullable|after_or_equal:start_date',
'notes' => 'string|nullable',
'cost' => 'numeric|nullable',
'cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
];
+8 -1
View File
@@ -12,7 +12,14 @@ trait HasUploads
return $this->hasMany(Actionlog::class, 'item_id')
->where('item_type', self::class)
->where('action_type', '=', 'uploaded')
->whereNotNull('filename');
->whereNotNull('filename')
->whereNotIn('filename', function ($query) {
$query->select('filename')
->from('action_logs')
->where('item_type', '=', self::class)
->where('action_type', '=', 'upload deleted')
->where('item_id', $this->id);
});
}
@@ -93,7 +93,7 @@ class CheckinAssetNotification extends Notification
return (new SlackMessage)
->content(':arrow_down: :computer: '.trans('mail.Asset_Checkin_Notification'))
->content(':arrow_down: :computer: '.trans('mail.Asset_Checkin_Notification', ['tag' => '']))
->from($botname)
->to($channel)
->attachment(function ($attachment) use ($item, $note, $admin, $fields) {
@@ -112,21 +112,21 @@ class CheckinAssetNotification extends Notification
return MicrosoftTeamsMessage::create()
->to($this->settings->webhook_endpoint)
->type('success')
->title(trans('mail.Asset_Checkin_Notification'))
->title(trans('mail.Asset_Checkin_Notification', ['tag' => '']))
->addStartGroupToSection('activityText')
->fact(htmlspecialchars_decode($item->display_name), '', 'activityText')
->fact(trans('mail.checked_into'), ($item->location) ? $item->location->name : '')
->fact(trans('mail.Asset_Checkin_Notification') . " by ", $admin->display_name)
->fact(trans('general.administrator'), $admin->display_name)
->fact(trans('admin/hardware/form.status'), $item->assetstatus?->name)
->fact(trans('mail.notes'), $note ?: '');
}
$message = trans('mail.Asset_Checkin_Notification');
$message = trans('mail.Asset_Checkin_Notification', ['tag' => '']);
$details = [
trans('mail.asset') => htmlspecialchars_decode($item->display_name),
trans('mail.checked_into') => ($item->location) ? $item->location->name : '',
trans('mail.Asset_Checkin_Notification')." by " => $admin->display_name,
trans('general.administrator') => $admin->display_name,
trans('admin/hardware/form.status') => $item->assetstatus?->name,
trans('mail.notes') => $note ?: '',
];
@@ -144,7 +144,7 @@ class CheckinAssetNotification extends Notification
->card(
Card::create()
->header(
'<strong>'.trans('mail.Asset_Checkin_Notification').'</strong>' ?: '',
'<strong>'.trans('mail.Asset_Checkin_Notification', ['tag' =>'']).'</strong>' ?: '',
htmlspecialchars_decode($item->display_name) ?: '',
)
->section(
@@ -110,7 +110,7 @@ class CheckoutAssetNotification extends Notification
}
return (new SlackMessage)
->content(':arrow_up: :computer: '.trans('mail.Asset_Checkout_Notification'))
->content(':arrow_up: :computer: '.trans('mail.Asset_Checkout_Notification', ['tag' => '']))
->from($botname)
->to($channel)
->attachment(function ($attachment) use ($item, $note, $admin, $fields) {
@@ -131,19 +131,19 @@ class CheckoutAssetNotification extends Notification
return MicrosoftTeamsMessage::create()
->to($this->settings->webhook_endpoint)
->type('success')
->title(trans('mail.Asset_Checkout_Notification'))
->title(trans('mail.Asset_Checkout_Notification', ['tag' => '']))
->addStartGroupToSection('activityText')
->fact(trans('mail.assigned_to'), $target->display_name)
->fact(htmlspecialchars_decode($item->display_name), '', 'activityText')
->fact(trans('mail.Asset_Checkout_Notification') . " by ", $admin->display_name)
->fact(trans('general.administrator'), $admin->display_name)
->fact(trans('mail.notes'), $note ?: '');
}
$message = trans('mail.Asset_Checkout_Notification');
$message = trans('mail.Asset_Checkout_Notification', ['tag' => '']);
$details = [
trans('mail.assigned_to') => $target->present()->name,
trans('mail.asset') => htmlspecialchars_decode($item->display_name),
trans('mail.Asset_Checkout_Notification'). ' by' => $admin->display_name,
trans('general.administrator') => $admin->display_name,
trans('mail.notes') => $note ?: '',
];
return array($message, $details);
@@ -159,7 +159,7 @@ public function toGoogleChat()
->card(
Card::create()
->header(
'<strong>'.trans('mail.Asset_Checkout_Notification').'</strong>' ?: '',
'<strong>'.trans('mail.Asset_Checkout_Notification', ['tag' => '']).'</strong>' ?: '',
htmlspecialchars_decode($item->display_name) ?: '',
)
->section(
@@ -50,11 +50,11 @@ class ExpectedCheckinNotification extends Notification
$message = (new MailMessage)->markdown('notifications.markdown.expected-checkin',
[
'date' => Helper::getFormattedDateObject($this->params->expected_checkin, 'date', false),
'asset' => $this->params->present()->name(),
'asset' => $this->params->display_name,
'serial' => $this->params->serial,
'asset_tag' => $this->params->asset_tag,
])
->subject(trans('mail.Expected_Checkin_Notification', ['name' => $this->params->present()->name()]));
->subject(trans('mail.Expected_Checkin_Notification', ['name' => $this->params->display_name]));
return $message;
}
+6 -2
View File
@@ -62,6 +62,10 @@ class ActionlogPresenter extends Presenter
return 'fa-solid fa-user-minus';
}
if ($this->action_type == 'upload deleted') {
return 'fa-solid fa-trash';
}
if ($this->action_type == 'update') {
return 'fa-solid fa-user-pen';
}
@@ -74,7 +78,7 @@ class ActionlogPresenter extends Presenter
return 'fa-solid fa-plus';
}
if ($this->action_type == 'delete') {
if (($this->action_type == 'delete') || ($this->action_type == 'upload deleted')) {
return 'fa-solid fa-trash';
}
@@ -141,7 +145,7 @@ class ActionlogPresenter extends Presenter
return $target->present()->nameUrl();
}
return '<del>'.$target->present()->name().'</del>';
return '<del>'.$target->display_name.'</del>';
}
return '';
+8
View File
@@ -143,6 +143,14 @@ class AssetModelPresenter extends Presenter
'title' => trans('admin/hardware/general.requestable'),
'formatter' => 'trueFalseFormatter',
],
[
'field' => 'require_serial',
'searchable' => false,
'sortable' => true,
'visible' => false,
'title' => trans('admin/hardware/general.require_serial'),
'formatter' => 'trueFalseFormatter',
],
[
'field' => 'notes',
'searchable' => true,
+1 -1
View File
@@ -32,11 +32,11 @@
"arietimmerman/laravel-scim-server": "dev-laravel_11_compatibility",
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-debugbar": "^3.13",
"barryvdh/laravel-dompdf": "^2.0",
"doctrine/cache": "^1.10",
"doctrine/dbal": "^3.1",
"doctrine/instantiator": "^1.3",
"eduardokum/laravel-mail-auto-embed": "^2.0",
"elibyy/tcpdf-laravel": "^11.5",
"enshrined/svg-sanitize": "^0.22.0",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.24",
Generated
+576 -697
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -300,7 +300,6 @@ return [
App\Providers\SnipeTranslationServiceProvider::class, //we REPLACE the default Laravel translator with our own
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
Barryvdh\DomPDF\ServiceProvider::class,
/*
* Package Service Providers...
@@ -315,6 +314,7 @@ return [
Unicodeveloper\DumbPassword\DumbPasswordServiceProvider::class,
Eduardokum\LaravelMailAutoEmbed\ServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class,
Elibyy\TCPDF\ServiceProvider::class,
/*
* Application Service Providers...
@@ -371,7 +371,7 @@ return [
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'PDF' => Elibyy\TCPDF\Facades\TCPDF::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
+17
View File
@@ -0,0 +1,17 @@
<?php
return [
'mode' => 'utf-8',
'format' => 'A4',
'author' => '',
'subject' => '',
'keywords' => '',
'creator' => 'Laravel Pdf',
'display_mode' => 'fullpage',
'tempDir' => base_path('../temp/'),
'pdf_a' => false,
'pdf_a_auto' => false,
'icc_profile_path' => '',
'defaultCssFile' => false,
'pdfWrapper' => 'misterspelik\LaravelPdf\Wrapper\PdfWrapper',
];
+1
View File
@@ -33,6 +33,7 @@ class AssetModelFactory extends Factory
'category_id' => Category::factory(),
'model_number' => $this->faker->creditCardNumber(),
'notes' => 'Created by demo seeder',
'require_serial' => 0,
];
}
@@ -14,6 +14,7 @@ class LicenseSeatFactory extends Factory
{
return [
'license_id' => License::factory(),
'unreassignable_seat' => false,
];
}
@@ -0,0 +1,26 @@
<?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('license_seats', function (Blueprint $table) {
$table->addColumn('boolean', 'unreassignable_seat')->default(false)->after('assigned_to');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('license_seats', function (Blueprint $table) {
$table->dropColumn('unreassignable_seat');
});
}
};
@@ -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->boolean( 'require_serial')->after('category_id')->default(0);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('models', function (Blueprint $table) {
$table->dropColumn('require_serial');
});
}
};
@@ -44,6 +44,8 @@ return [
'redirect_to_checked_out_to' => 'Go to Checked Out to',
'select_statustype' => 'Select Status Type',
'serial' => 'Serial',
'serial_required' => 'Asset :number requires a serial number',
'serial_required_post_model_update' => ':asset_model have been updated to require a serial number. Please add a serial number for this asset.',
'status' => 'Status',
'tag' => 'Asset Tag',
'update' => 'Asset Update',
@@ -22,6 +22,8 @@ return [
'requested' => 'Requested',
'not_requestable' => 'Not Requestable',
'requestable_status_warning' => 'Do not change requestable status',
'require_serial' => 'Require Serial Number',
'require_serial_help' => 'A serial number will be required when creating a new asset of this model.',
'restore' => 'Restore Asset',
'pending' => 'Pending',
'undeployable' => 'Undeployable',
@@ -50,7 +50,7 @@ return array(
'checkin' => array(
'error' => 'There was an issue checking in the license. Please try again.',
'not_reassignable' => 'License not reassignable',
'not_reassignable' => 'Seat has been used',
'success' => 'The license was checked in successfully'
),
+1
View File
@@ -611,6 +611,7 @@ return [
'use_cloned_no_image_help' => 'This item does not have an associated image and instead inherits from the model or category it belongs to. If you would like to use a specific image for this item, you can upload a new one below.',
'footer_credit' => '<a target="_blank" href="https://snipeitapp.com" rel="noopener">Snipe-IT</a> is open source software, made with <i class="fa fa-heart" aria-hidden="true" style="color: #a94442; font-size: 10px" /></i><span class="sr-only">love</span> by <a href="https://bsky.app/profile/snipeitapp.com" rel="noopener">@snipeitapp.com</a>.',
'set_password' => 'Set a Password',
'upload_deleted' => 'Upload Deleted',
// Add form placeholders here
'placeholders' => [
+2 -2
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: [:tag]',
'Asset_Checkout_Notification' => 'Asset checked out: [:tag]',
'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',
@@ -80,12 +80,10 @@
<!-- checkout selector -->
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_user'])
<!-- We have to pass unselect here so that we don't default to the asset that's being checked out. We want that asset to be pre-selected everywhere else. -->
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'company_id' => $accessory->company_id, 'fieldname' => 'assigned_user', 'style' => session('checkout_to_type') == 'user' ? '' : 'display: none;'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'asset_selector_div_id' => 'assigned_asset', 'company_id' => $accessory->company_id, 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => session('checkout_to_type') == 'asset' ? '' : 'display: none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'company_id' => $accessory->company_id, 'style' => session('checkout_to_type') == 'location' ? '' : 'display: none;'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.select_asset'), 'fieldname' => 'assigned_asset', 'company_id' => $accessory->company_id, 'unselect' => 'true', 'style' => 'display:none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;'])
<!-- Checkout QTY -->
@@ -1,9 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"
dir="{{ Helper::determineLanguageDirection() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style>
body {
font-family:'Dejavu Sans', Arial, Helvetica, sans-serif;
@@ -31,7 +33,12 @@
@if ($eula)
<hr>
{!! $eula !!}
{!! $eula !!}
{!! str_replace('<p>', '<p dir="auto">', $eula) !!}
{{ str_replace('<p>', '<p dir="auto">', $eula) }}
<hr>
@endif
@@ -46,7 +46,7 @@
@if ($acceptance->checkoutable->getEula())
<div class="col-md-12" style="padding-top: 15px; padding-bottom: 15px;">
<div style="background-color: rgba(211,211,211,0.25); padding: 10px; border: lightgrey 1px solid;">
{!! $acceptance->checkoutable->getEula() !!}
{!! str_replace('<p>', '<p dir="auto">', Helper::parseEscapedMarkedown($acceptance->checkoutable->getEula())) !!}
</div>
</div>
@endif
@@ -26,7 +26,7 @@
<div class="box-body">
<!-- Asset -->
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.select_asset'), 'fieldname' => 'asset_id', 'company_id' => $component->company_id])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.select_asset'), 'fieldname' => 'asset_id', 'company_id' => $component->company_id, 'required' => 'true', 'value' => old('asset_id')])
<div class="form-group {{ $errors->has('assigned_qty') ? ' has-error' : '' }}">
<label for="assigned_qty" class="col-md-3 control-label">
@@ -58,11 +58,13 @@
<!-- Checkout selector -->
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'asset_selector_div_id' => 'assigned_asset', 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;'])
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user', 'style' => session('checkout_to_type') == 'user' ? '' : 'display: none;'])
<!-- We have to pass unselect here so that we don't default to the asset that's being checked out. We want that asset to be pre-selected everywhere else. -->
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'asset_selector_div_id' => 'assigned_asset', 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => session('checkout_to_type') == 'asset' ? '' : 'display: none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => session('checkout_to_type') == 'location' ? '' : 'display: none;'])
<!-- Checkout/Checkin Date -->
<div class="form-group {{ $errors->has('checkout_at') ? 'error' : '' }}">
+3 -6
View File
@@ -94,13 +94,10 @@
</div>
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user', 'style' => session('checkout_to_type') == 'user' ? '' : 'display: none;'])
<!-- We have to pass unselect here so that we don't default to the asset that's being checked out. We want that asset to be pre-selected everywhere else. -->
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.select_asset'), 'fieldname' => 'assigned_asset', 'company_id' => $asset->company_id, 'unselect' => 'true', 'style' => session('checkout_to_type') == 'asset' ? '' : 'display: none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => session('checkout_to_type') == 'location' ? '' : 'display: none;'])
+3 -6
View File
@@ -56,12 +56,9 @@
</div>
@endcan
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_to'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('admin/licenses/form.asset'), 'fieldname' => 'asset_id', 'style' => 'display:none;'])
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'false'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_to', 'style' => session('checkout_to_type') == 'user' ? '' : 'display: none;'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.select_asset'), 'fieldname' => 'asset_id', 'style' => session('checkout_to_type') == 'asset' ? '' : 'display: none;'])
<!-- Note -->
<div class="form-group {{ $errors->has('notes') ? 'error' : '' }}">
+7 -14
View File
@@ -582,20 +582,13 @@
{{ trans('admin/licenses/general.bulk.checkin_all.button') }}
</a>
</span>
@elseif (! $license->reassignable)
<span data-tooltip="true" title=" {{ trans('admin/licenses/general.bulk.checkin_all.disabled_tooltip_reassignable') }}">
<a href="#" class="btn btn-primary bg-purple btn-sm btn-social btn-block hidden-print disabled" style="margin-bottom: 25px;">
<x-icon type="checkin" />
{{ trans('admin/licenses/general.bulk.checkin_all.button') }}
</a>
</span>
@else
<a href="#" class="btn btn-primary bg-purple btn-sm btn-social btn-block hidden-print" style="margin-bottom: 25px;" data-toggle="modal" data-tooltip="true" data-target="#checkinFromAllModal" data-content="{{ trans('general.sure_to_delete') }}" data-title="{{ trans('general.delete') }}" onClick="return false;">
<x-icon type="checkin" />
{{ trans('admin/licenses/general.bulk.checkin_all.button') }}
</a>
@endif
@endcan
@else
<a href="#" class="btn btn-primary bg-purple btn-sm btn-social btn-block hidden-print" style="margin-bottom: 25px;" data-toggle="modal" data-tooltip="true" data-target="#checkinFromAllModal" data-content="{{ trans('general.sure_to_delete') }} data-title="{{ trans('general.delete') }}" onClick="return false;">
<x-icon type="checkin" />
{{ trans('admin/licenses/general.bulk.checkin_all.button') }}
</a>
@endif
@endcan
@can('delete', $license)
+2 -2
View File
@@ -55,7 +55,7 @@
@endif
<br>
@if ($company)
<b>{{ trans('admin/companies/table.name') }}:</b> {{ $company->present()->Name() }}</b>
<b>{{ trans('admin/companies/table.name') }}:</b> {{ $company->display_name }}
<br>
@endif
@if ($manager)
@@ -142,7 +142,7 @@
<td>{{ (($asset->model) && ($asset->model->manufacturer)) ? $asset->model->manufacturer->name : '' }}</td>
<td>{{ ($asset->model) ? $asset->model->name : '' }}</td>
<td>{{ $asset->serial }}</td>
<td>{{ $asset->location->name }}</td>
<td>{{ ($asset->location) ? $asset->location->name : '' }}</td>
<td>{{ \App\Helpers\Helper::getFormattedDateObject( $asset->last_checkout, 'datetime', false) }}</td>
<td>{{ \App\Helpers\Helper::getFormattedDateObject( $asset->expected_checkin, 'datetime', false) }}</td>
</tr>
@@ -11,7 +11,7 @@
| | |
| ------------- | ------------- |
| **{{ trans('mail.user') }}** | {{ $assignedTo->display_name }} |
| **{{ trans('mail.name') }}** | {{ $item->present()->name() }} |
| **{{ trans('mail.name') }}** | {{ $item->display_name }} |
@if (isset($item->asset_tag))
| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} |
@endif
+5 -4
View File
@@ -105,8 +105,6 @@
@include ('partials.forms.edit.maintenance_type')
@include ('partials.forms.edit.supplier-select', ['translated_name' => trans('general.supplier'), 'fieldname' => 'supplier_id'])
<!-- Start Date -->
<div class="form-group {{ $errors->has('start_date') ? ' has-error' : '' }}">
@@ -142,6 +140,9 @@
</div>
</div>
@include ('partials.forms.edit.supplier-select', ['translated_name' => trans('general.supplier'), 'fieldname' => 'supplier_id'])
<!-- Warranty -->
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
@@ -155,7 +156,7 @@
<!-- Asset Maintenance Cost -->
<div class="form-group {{ $errors->has('cost') ? ' has-error' : '' }}">
<label for="cost" class="col-md-3 control-label">{{ trans('admin/maintenances/form.cost') }}</label>
<div class="col-md-2">
<div class="col-md-3">
<div class="input-group">
<span class="input-group-addon">
@if (($item->asset) && ($item->asset->location) && ($item->asset->location->currency!=''))
@@ -164,7 +165,7 @@
{{ $snipeSettings->default_currency }}
@endif
</span>
<input class="col-md-2 form-control" type="text" name="cost" id="cost" value="{{ old('cost', Helper::formatCurrencyOutput($item->cost)) }}" />
<input class="form-control" type="number" name="cost" min="0.00" max="99999999999999999.000" step="0.001" aria-label="cost" id="cost" value="{{ old('cost', $item->cost) }}" maxlength="25" />
{!! $errors->first('cost', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
@@ -91,7 +91,27 @@
</div>
@include ('partials.forms.edit.minimum_quantity')
<!-- require serial boolean -->
<div class="form-group">
<label for="require_serial" class="col-md-3 control-label">
{{ trans('admin/hardware/general.require_serial') }}
</label>
<div class="col-md-9">
<div class="form-inline" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" name="require_serial" value="1" id="require_serial" aria-label="require_serial" />
<a
href="#"
data-tooltip="true"
title="{{ trans('admin/hardware/general.require_serial_help') }}"
style="display: inline-flex; align-items: center;"
>
<x-icon type="info-circle" />
<span class="sr-only">{{ trans('admin/hardware/general.require_serial_help') }}</span>
</a>
</div>
</div>
</div>
<!-- requestable -->
<div class="form-group{{ $errors->has('requestable') ? ' has-error' : '' }}">
+21
View File
@@ -16,6 +16,27 @@
@include ('partials.forms.edit.depreciation')
@include ('partials.forms.edit.minimum_quantity')
<!-- require serial boolean -->
<div class="form-group">
<label for="require_serial" class="col-md-3 control-label">
{{ trans('admin/hardware/general.require_serial') }}
</label>
<div class="col-md-9">
<div class="form-inline" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" name="require_serial" value="1" @checked(old('require_serial', $item->require_serial)) id="require_serial" aria-label="require_serial" />
<a
href="#"
data-tooltip="true"
title="{{ trans('admin/hardware/general.require_serial_help') }}"
style="display: inline-flex; align-items: center;"
>
<x-icon type="info-circle" />
<span class="sr-only">{{ trans('admin/hardware/general.require_serial_help') }}</span>
</a>
</div>
</div>
</div>
<!-- EOL -->
<div class="form-group {{ $errors->has('eol') ? ' has-error' : '' }}">
@@ -2,16 +2,24 @@
{{ trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count'=>$assets->count(), 'threshold' => $threshold]) }}
@component('mail::table')
<table width="100%">
<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->display_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()->display_name) : '') }} </td></tr>
@component('mail::table')
| | | |
| ------------- | ------------- | ------------- |
| {{ $icon }} **{{ trans('mail.name') }}** | <a href="{{ route('hardware.show', $asset->id) }}">{{ $asset->display_name }}</a> <br><small>{{trans('mail.serial').': '.$asset->serial}}</small> |
| **{{ trans('mail.expires') }}** | {{ !is_null($expires) ? $expires['formatted'] : '' }} (<strong>{{ $diff }} {{ trans('mail.Days') }}</strong>) |
@if ($asset->supplier)
| **{{ trans('mail.supplier') }}** | {{ ($asset->supplier ? e($asset->supplier->name) : '') }} |
@endif
@if ($asset->assignedTo)
| **{{ trans('mail.assigned_to') }}** | {{ e($asset->assignedTo->present()->display_name) }} |
@endif
@endcomponent
@endforeach
</table>
@endcomponent
@endcomponent
@@ -338,6 +338,7 @@
'deployed': '{{ strtolower(trans('general.deployed')) }}',
'deployable': '{{ strtolower(trans('admin/hardware/general.deployable')) }}',
'archived': '{{ strtolower(trans('general.archived')) }}',
'undeployable': '{{ strtolower(trans('general.undeployable')) }}',
'pending': '{{ strtolower(trans('general.pending')) }}'
}
@@ -560,13 +561,16 @@
// Checkouts need the license ID, checkins need the specific seat ID
function licenseSeatInOutFormatter(value, row) {
if(row.disabled) {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-maroon disabled" data-tooltip="true" title="{{ trans('general.checkin_tooltip') }}">{{ trans('general.checkout') }}</a>';
} else
// The user is allowed to check the license seat out and it's available
if ((row.available_actions.checkout === true) && (row.user_can_checkout === true) && ((!row.asset_id) && (!row.assigned_to))) {
return '<a href="{{ config('app.url') }}/licenses/' + row.license_id + '/checkout/'+row.id+'" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
} else {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="{{ trans('general.checkin_tooltip') }}">{{ trans('general.checkin') }}</a>';
}
else {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="{{ trans('general.checkin_tooltip') }}">{{ trans('general.checkin') }}</a>';
}
}
function genericCheckinCheckoutFormatter(destination) {
@@ -1,20 +1,27 @@
<div class="form-group" id="assignto_selector"{!! (isset($style)) ? ' style="'.e($style).'"' : '' !!}>
<label for="checkout_to_type" class="col-md-3 control-label">{{ trans('admin/hardware/form.checkout_to') }}</label>
<div class="col-md-8">
<div class="btn-group" data-toggle="buttons">
@if ((isset($user_select)) && ($user_select!='false'))
<label class="btn btn-default active">
<input name="checkout_to_type" value="user" aria-label="checkout_to_type" type="radio" checked="checked"><x-icon type="user" /> {{ trans('general.user') }}
<label class="btn btn-default{{ session('checkout_to_type') == 'user' ? ' active' : '' }}">
<input name="checkout_to_type" value="user" aria-label="checkout_to_type" type="radio" checked="checked">
<x-icon type="user" />
{{ trans('general.user') }}
</label>
@endif
@if ((isset($asset_select)) && ($asset_select!='false'))
<label class="btn btn-default">
<input name="checkout_to_type" value="asset" aria-label="checkout_to_type" type="radio"><i class="fas fa-barcode" aria-hidden="true"></i> {{ trans('general.asset') }}
<label class="btn btn-default{{ session('checkout_to_type') == 'asset' ? ' active' : '' }}">
<input name="checkout_to_type" value="asset" aria-label="checkout_to_type" type="radio">
<i class="fas fa-barcode" aria-hidden="true"></i>
{{ trans('general.asset') }}
</label>
@endif
@if ((isset($location_select)) && ($location_select!='false'))
<label class="btn btn-default">
<input name="checkout_to_type" value="location" aria-label="checkout_to_type" class="active" type="radio"><i class="fas fa-map-marker-alt" aria-hidden="true"></i> {{ trans('general.location') }}
<label class="btn btn-default{{ session('checkout_to_type') == 'location' ? ' active' : '' }}">
<input name="checkout_to_type" value="location" aria-label="checkout_to_type" class="active" type="radio">
<i class="fas fa-map-marker-alt" aria-hidden="true"></i>
{{ trans('general.location') }}
</label>
@endif
@@ -2,8 +2,8 @@
<div class="form-group {{ $errors->has('purchase_cost') ? ' has-error' : '' }}">
<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', $item->purchase_cost) }}" maxlength="24" />
<div class="input-group col-md-5" style="padding-left: 0px;">
<input class="form-control" type="number" name="purchase_cost" min="0.00" max="99999999999999999.000" step="0.001" aria-label="purchase_cost" id="purchase_cost" value="{{ old('purchase_cost', $item->purchase_cost) }}" maxlength="25" />
<span class="input-group-addon">
@if (isset($currency_type))
{{ $currency_type }}
@@ -2,7 +2,11 @@
<div class="form-group {{ $errors->has('serial') ? ' has-error' : '' }}">
<label for="{{ $fieldname }}" class="col-md-3 control-label">{{ trans('admin/hardware/form.serial') }} </label>
<div class="col-md-7 col-sm-12">
<input class="form-control" type="text" name="{{ $fieldname }}" id="{{ $fieldname }}" value="{{ old((isset($old_val_name) ? $old_val_name : $fieldname), $item->serial) }}"{{ (Helper::checkIfRequired($item, 'serial')) ? ' required' : '' }} maxlength="191" />
{!! $errors->first('serial', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
<input class="form-control" type="text" name="{{ $fieldname }}" id="{{ $fieldname }}" value="{{ old((isset($old_val_name) ? $old_val_name : $fieldname), $item->serial) }}" {{ (Helper::checkIfRequired($item, 'serial') || ($item->model && $item->model->require_serial)) ? ' required' : '' }} maxlength="191" />
@error($old_val_name ?? $fieldname)
<span class="alert-msg" aria-hidden="true">
<i class="fas fa-times" aria-hidden="true"></i> {{ $message }}
</span>
@enderror
</div>
</div>
+1 -1
View File
@@ -137,7 +137,7 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
/**
* Categpries API routes
* Categories API routes
*/
Route::group(['prefix' => 'categories'], function () {
@@ -2,6 +2,8 @@
namespace Tests\Feature\Assets\Ui;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\User;
use Tests\TestCase;
@@ -13,4 +15,63 @@ class StoreAssetsTest extends TestCase
->get(route('hardware.create'))
->assertOk();
}
public function testAssetCanBeStoredWithSerialRequiredAndSerialProvided()
{
$user = User::factory()->superuser()->create();
$this->actingAs($user);
$model = AssetModel::factory()->create([
'require_serial' => 1,
]);
$response = $this->post(route('hardware.store'), [
'model_id' => $model->id,
'serials' => [1 => 'ABC123'],
'asset_tags' =>[1 => '1234'],
'status_id' => 1,
// other required fields...
]);
$response->assertRedirect();
$response->assertSessionHas('success-unescaped');
$this->assertNotEquals(
trans('admin/hardware/form.serial_required'),
session('error')
);
$this->assertDatabaseHas('assets', [
'model_id' => $model->id,
'serial' => 'ABC123',
'asset_tag' => '1234',
]);
}
public function testAssetCannotBeStoredIfSerialRequiredAndMissing()
{
$user = User::factory()->superuser()->create();
$this->actingAs($user);
$model = AssetModel::factory()->create([
'require_serial' => 1,
]);
$response = $this->post(route('hardware.store'), [
'model_id' => $model->id,
'serials' => [], // ← serial missing
'asset_tags' => [1 => '1234'],
'status_id' => 1,
]);
$response->assertRedirect();
$response->assertSessionHasErrors(['serials.1']);
$this->assertDatabaseMissing('assets', [
'model_id' => $model->id,
'asset_tag' => '1234',
]);
$response->assertSessionMissing('success-unescaped');
}
}
@@ -20,7 +20,7 @@ class LicenseCheckinTest extends TestCase
->assertForbidden();
}
public function testCannotCheckinNonReassignableLicense()
public function testNonReassignableLicenseSeatCantBeCheckedOut()
{
$licenseSeat = LicenseSeat::factory()
->notReassignable()
@@ -28,13 +28,11 @@ class LicenseCheckinTest extends TestCase
->create();
$this->actingAs(User::factory()->checkoutLicenses()->create())
->post(route('licenses.checkin.save', $licenseSeat), [
'notes' => 'my note',
'redirect_option' => 'index',
])
->assertSessionHas('error', trans('admin/licenses/message.checkin.not_reassignable') . '.');
->post(route('licenses.checkin.save', $licenseSeat));
$this->assertNotNull($licenseSeat->fresh()->assigned_to);
$licenseSeat->refresh();
$this->assertEquals(true, $licenseSeat->unreassignable_seat);
}
public function testCannotCheckinLicenseThatIsNotAssigned()
@@ -113,28 +113,28 @@ class ImportAssetModelsTest extends ImportDataTestCase implements TestsPermissio
#[Test]
public function updateAssetModelFromImport(): void
{
$assetmodel = AssetModel::factory()->create()->refresh();
$category = Category::find($assetmodel->category->name);
$importFileBuilder = ImportFileBuilder::new(['name' => $assetmodel->name, 'model_number' => Str::random(), 'category' => $category]);
$assetmodel = AssetModel::factory()->create();
$category = Category::find($assetmodel->category_id);
$importFileBuilder = ImportFileBuilder::new(['name' => $assetmodel->name, 'model_number' => Str::random(), 'category' => $category->name]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->assetmodel()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$this->importFileResponse(['import' => $import->id, 'import-update' => true])
->assertOk()
->assertExactJson([
'payload' => null,
'status' => 'success',
'messages' => ['redirect_url' => route('models.index')]
]);
$updatedAssetmodel = AssetModel::query()->find($assetmodel->id);
$updatedAttributes = [
'name',
'model_number'
];
$this->assertEquals($row['model_number'], $updatedAssetmodel->model_number);
$this->assertEquals($row['name'], $updatedAssetmodel->name);
$this->assertEquals(
Arr::except($assetmodel->attributesToArray(), array_merge($updatedAttributes, $assetmodel->getDates())),
Arr::except($updatedAssetmodel->attributesToArray(), array_merge($updatedAttributes, $assetmodel->getDates())),
);
}
}