Compare commits

...

679 Commits

Author SHA1 Message Date
snipe
943a4093ad Use standard model transformer for asset model API response
Signed-off-by: snipe <snipe@snipe.net>
2025-07-10 15:42:39 +01:00
snipe
b0917a5131 Merge pull request #17385 from grokability/17383-fix-api-route-path
Fixed #17383 - re-add `/hardware/` as an object type in the file upload API
2025-07-10 13:10:43 +01:00
snipe
0972c4e340 Re-added /hardware/ as viable route for API file uploads
Signed-off-by: snipe <snipe@snipe.net>
2025-07-10 13:06:22 +01:00
snipe
43a237bf95 Merge pull request #17378 from grokability/phpcs/models
Code formatting fixes
2025-07-09 21:55:30 +01:00
snipe
95f867b267 Code formatting fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 21:48:53 +01:00
snipe
e96daf469a Better phrasing
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 21:00:54 +01:00
snipe
f2cdfe9e47 Normalize textarea for notes in acceptance form
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 20:58:07 +01:00
snipe
929b67e768 Merge pull request #17376 from grokability/small-tweak-to-acceptance-ui
Better indicate via submit button colors and messaging that something is about to be accepted or declined
2025-07-09 20:21:50 +01:00
snipe
0573dc136a Put the sig check back
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 20:16:30 +01:00
snipe
48588f6a9e Small UI sugar on the acceptance/signature screen
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 20:08:19 +01:00
snipe
88579b9bf3 Merge pull request #17374 from uberbrady/improve_inline_videos
Fixed [FD-49538 ] - use a Video tag for video files for non-Safari usage
2025-07-09 15:47:19 +01:00
snipe
e8bb9bde99 Fixed #8201 - splits first_name and last_name in user export
Signed-off-by: snipe <snipe@snipe.net>
2025-07-09 15:46:42 +01:00
Brady Wetherington
0ee3cca4da Use a Video tag for video files for non-Safari usage 2025-07-09 15:15:53 +01:00
snipe
f89ee6b7f2 Merge pull request #17361 from Godmartinz/return-custom-textarea_input
Fixed #7957 - custom field Textarea input not retaining when switching Asset Models with shared fields
2025-07-08 22:02:07 +01:00
snipe
aebfb52c85 Merge pull request #17362 from Godmartinz/license-redirect-bug
Fixed #17310 - 500 on redirect when checking in a license seat
2025-07-08 21:59:22 +01:00
Godfrey M
667bd7af0e fix checkout_to_type being null when checking in 2025-07-08 13:53:10 -07:00
snipe
bbf69bc582 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-07-08 21:26:15 +01:00
snipe
f2b7a3d002 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-07-08 21:25:31 +01:00
Godfrey M
3fd9e3ab56 include textareas input return 2025-07-08 12:02:56 -07:00
snipe
8e11466a54 Added query scope
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 22:09:23 +01:00
snipe
dade9797d5 Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 21:46:13 +01:00
snipe
97c1e65ffc Fixed fieldname
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 20:58:26 +01:00
snipe
b4e22f4a21 Small fix for seat listing
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 20:56:26 +01:00
snipe
58b6feb3ca Merge pull request #17356 from grokability/show-only-taken-licenses
[FD-49569 ] - Show only assigned in license tab
2025-07-07 17:05:38 +01:00
snipe
41c4920d45 Show only assigned in license tab
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 16:58:36 +01:00
snipe
d1ddd8de98 Re-add column selector
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 16:23:48 +01:00
snipe
f41307eb4a Merge pull request #17353 from grokability/fixes-#14295-send-acceptance-on-signing
Fixed #14295 - allow user to receive an email PDF upon signing
2025-07-07 15:59:03 +01:00
snipe
59de77feb0 Use company name if provided instead of site name
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 15:55:40 +01:00
snipe
8ebbcf6e80 Removed console commands
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 15:55:31 +01:00
snipe
24c6e836dd Added checkbox toggle
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 15:51:24 +01:00
snipe
8e38b3898e Fixes #14295 - allow user to receive an email PDF upon signing
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 15:38:36 +01:00
snipe
ce9a5e35c9 Merge pull request #17352 from grokability/fixed-#17273-notes-in-upcoming-audit
Fixed #17273 -  switch to HTML table from markdown
2025-07-07 14:42:43 +01:00
snipe
b092779697 Fixed #17273 - switch to HTML table from markdown
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 14:37:38 +01:00
snipe
ab30a96d16 Merge pull request #17327 from Godmartinz/asset_model_redirect
FIXED: #15861 adds a redirect option for asset model and previous page
2025-07-07 14:00:31 +01:00
snipe
dab0fb16ad Merge pull request #17291 from Godmartinz/fix_bulk_checkout_focus
Fixes #12094 - Adds focus to select2 in bulk checkout
2025-07-07 13:57:12 +01:00
snipe
5be398bc99 Merge pull request #17350 from grokability/tighter-control-on-company
Fixes #17302 - Tighter control on company
2025-07-07 13:45:08 +01:00
snipe
fe4172957f Merge pull request #17351 from grokability/smaller-pdfs
Fixed #17349 - enable_font_subsetting in PDFs
2025-07-07 13:33:29 +01:00
snipe
ff3a59d347 Fixed #17349 - enable_font_subsetting in PDFs
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 13:32:25 +01:00
snipe
f9aedea26f Eager load admin
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 13:21:07 +01:00
snipe
5abd2c7151 Added tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 13:12:16 +01:00
snipe
bfcaf4f37b Removed unecessary use statement
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 12:36:59 +01:00
snipe
5f4e1835bc Removed unused method
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 12:36:16 +01:00
snipe
c1f1ae6b64 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 12:31:12 +01:00
snipe
c4fcc6c24e Removed direct scoping calls
Signed-off-by: snipe <snipe@snipe.net>
2025-07-07 12:26:30 +01:00
snipe
dd73ad9941 Merge pull request #17341 from Godmartinz/query_error_rb19824
Fixed #17193: perform Orderby before Collection in Bulk Assets Controller
2025-07-07 11:19:37 +01:00
snipe
ac21f7569f Merge pull request #17346 from grokability/add-video-uploads
[FD-49538] Add video/audio uploads
2025-07-07 11:15:19 +01:00
snipe
4ef0158da4 Use preview instead of image text
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:19:29 +01:00
snipe
4db3b3ba0e Use config array for extensions in restore tool
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:19:13 +01:00
snipe
dc43d85323 Check for audio files
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:15:30 +01:00
snipe
62651f381c Expand safe allowed inline files
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:13:57 +01:00
snipe
3e9098907a Use config file for file types/mimes
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:11:31 +01:00
snipe
e18df250f8 Removed console debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-03 20:08:08 +01:00
Godfrey M
be5c5a51da get desired behavior of select-2 2025-07-02 11:20:04 -07:00
Godfrey M
a728fad675 perform orderBy on query before converting to a Collection 2025-07-02 11:06:35 -07:00
snipe
185629b310 Merge pull request #17338 from grokability/small-depreciation-tweaks
Fixed #1909 - Small depreciation tweaks
2025-07-02 18:37:21 +01:00
snipe
30ebea4f2d Return int count
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 18:24:27 +01:00
snipe
b135c1eac2 Updated language strings in view
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 18:15:06 +01:00
snipe
88fef73d6f Use new translation
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:54:10 +01:00
snipe
556a9039e9 New strings
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:53:55 +01:00
snipe
cdfe6c21c1 Tightened HTML, added translations
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:53:42 +01:00
snipe
b094ebdd66 Removed validation of > 0
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:53:32 +01:00
snipe
526a7ddea6 Merge pull request #17337 from grokability/fixes-#17112-ldap-location-set-to-0
Fixed #17112 - Set location ID to null instead of 0
2025-07-02 17:20:34 +01:00
snipe
bb5ad31cba Merge pull request #17336 from uberbrady/safer_deserialize
Use safer deserialization defaults
2025-07-02 17:15:48 +01:00
snipe
549da2efed Set location ID to null instead of 0
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:15:18 +01:00
snipe
e5e586dc43 Attempt to generalize companyable in company scope
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 17:12:55 +01:00
Brady Wetherington
8a682beb0e Use safer deserialization defaults 2025-07-02 14:49:12 +01:00
snipe
699e9f75c9 Fixed RB-19892
Check for location before trying to grab company property

Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 11:57:42 +01:00
snipe
759e30977b Merge pull request #17333 from grokability/fixes-#17326-dash-sorting
Fixed #17326 - sorting on dashboard
2025-07-02 11:43:48 +01:00
snipe
6cfdb49cc3 Fixed #17326 - sorting on dashboard
Signed-off-by: snipe <snipe@snipe.net>
2025-07-02 11:41:21 +01:00
snipe
1195121bf0 Merge pull request #17330 from uberbrady/add_escaping_to_action_logs
Add escaping to user_agent and remote_ip variables for API results
2025-07-01 23:30:41 +01:00
Brady Wetherington
8bc067b18b Add escaping to user_agent and remote_ip variables for API results 2025-07-01 23:22:09 +01:00
Godfrey M
76f59f7b85 fix variables 2025-07-01 12:52:16 -07:00
Godfrey M
55ebb4671f update check in and check out controllers 2025-07-01 12:44:26 -07:00
Godfrey M
8a9cf07063 editing controllers and edit blades for other categories 2025-07-01 12:31:55 -07:00
Godfrey M
ca9ff8cf19 set return type for RedirectOptions 2025-07-01 12:04:02 -07:00
Godfrey M
7217d9c427 adds redirect to previous page, use match instead of switch cases 2025-07-01 11:55:46 -07:00
Godfrey M
9d712ad8f1 clean up code 2025-07-01 10:56:02 -07:00
Godfrey M
f3e49e7010 add asset model as a redirect option 2025-07-01 10:43:00 -07:00
snipe
ba94f1b920 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-06-30 11:45:43 +01:00
snipe
edcd46dd67 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-06-30 11:44:46 +01:00
snipe
5cf6c89dde Merge pull request #17322 from grokability/fixes-#8484-add-supplier-to-license-relationship
Fixed #8484 - added supplier to license relationship
2025-06-30 11:29:07 +01:00
snipe
58676b1f83 Fixed #8484 - added supplier to license relationship
Signed-off-by: snipe <snipe@snipe.net>
2025-06-30 11:27:30 +01:00
snipe
8ff7c30e5a Merge pull request #17315 from grokability/traitify-uploads
Added HasUploads trait and remove uploads method for models
2025-06-27 20:00:28 +01:00
snipe
cd989768d4 Added HasUploads trait and remove uploads method for models
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 19:32:22 +01:00
snipe
6cbdefe3d9 Small regressions
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 13:04:02 +01:00
snipe
c6ecc0d8e8 Merge pull request #17311 from grokability/rwork-bulk-api-to-smaller-pr
Fixed #9413 and rework upload API for bulk and better responses (refactor of #16964)
2025-06-27 12:43:34 +01:00
snipe
e0f5663bf4 Requested changes
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 12:37:11 +01:00
snipe
aafc8996c1 phpcbf fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 12:18:54 +01:00
snipe
128da40cbf Added comments
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 12:03:12 +01:00
snipe
ea0460e97e Remove unused API files controllers
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 11:51:36 +01:00
snipe
d8e7123576 Added uploaded files API controllers and presenters
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 11:37:31 +01:00
snipe
6f45ec655f Merge pull request #16857 from realchrisolin/generic_tape
Generic tape
2025-06-25 14:59:57 +01:00
snipe
ec17c168ea Merge pull request #17222 from grokability/fixes-#17221-move-table-featueres-into-js
Fixed #17221 - Moved common table elements to partial
2025-06-25 14:52:17 +01:00
snipe
119b097521 More cleanup
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:43:09 +01:00
snipe
c731633a84 Fixed weird formatting
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:40:26 +01:00
snipe
6a4d6ade39 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:39:46 +01:00
snipe
b3b4697fc9 More whitespace cleanup
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:38:25 +01:00
snipe
72c706d697 Aaaand one more
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:30:34 +01:00
snipe
8a7af24bd4 More whitespace
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:29:33 +01:00
snipe
dd01bd3e5f Cleaned up whitespace
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:29:01 +01:00
snipe
59cade9f82 Cleaned up whitespace
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:28:20 +01:00
snipe
6bb9b79832 Tweaked dashboard
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:11:29 +01:00
snipe
f8fe7b5803 Removed whitespace
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:11:19 +01:00
snipe
4d6279d61c Added JS to handle data-dash attribute overrides
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:11:04 +01:00
snipe
5ef581f328 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 14:10:51 +01:00
snipe
20a59c343e Merge pull request #17296 from uberbrady/improve_fmcs_locations_test
Fixed #17190 and [FD-49375]: Do FMCS testing 'async' to keep from blowing out the whole settings page
2025-06-25 13:10:54 +01:00
snipe
d6feb522b7 Merge pull request #17297 from grokability/fixes-#17259-#15239-external-avatar
Fixed #15239 and #17259 - better handle external avatars
2025-06-25 13:09:33 +01:00
snipe
951aee8292 Fixes #15239 and #17259 - better handle external avatars
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 13:03:51 +01:00
Brady Wetherington
6e2d7912b5 Do the FMCS testing 'async' to keep from blowing out the whole page 2025-06-25 12:59:37 +01:00
snipe
b20925b550 Fixed #17282 - removed erroneous update gate for user-license endpoint
Signed-off-by: snipe <snipe@snipe.net>
2025-06-25 11:03:06 +01:00
Godfrey M
483f684b04 adds focus to select 2 bulk checkout 2025-06-24 09:58:38 -07:00
snipe
26774b4193 Merge pull request #17287 from uberbrady/limit_license_seat_increments
Fixed [FD-45786] - Limit changing of asset seat count to no more than 10k at a time
2025-06-24 17:43:02 +01:00
Brady Wetherington
a7a597d609 Added some tests around license seat changes 2025-06-24 17:35:49 +01:00
snipe
1c12b9278a Merge pull request #17116 from marcusmoore/17065-send-alert-to-assigner-upon-response
Fixed #17065 - allow sending acceptance alert to initiator.
2025-06-24 17:30:47 +01:00
Brady Wetherington
de4764bd05 Limit changing of asset seat count to no more than 10k at a time 2025-06-24 13:53:11 +01:00
snipe
c1e7a78d23 Merge pull request #17284 from Godmartinz/fix-depreciation-choice-in-transformer
FIXED: #14869 changes the depreciation method selected for Assets index table
2025-06-23 20:49:25 +01:00
Godfrey M
8894bb91cc fix method choice in asset transformer for depreciations 2025-06-23 12:37:29 -07:00
snipe
c955126f01 Merge pull request #17283 from Godmartinz/multiclick_checkout_bug
Fixed #14077: Disables checkout button after submitting
2025-06-23 20:30:38 +01:00
Godfrey M
ce53b48d04 disable checkout button after submitting 2025-06-23 12:18:24 -07:00
snipe
6015aeddee Merge pull request #17209 from Godmartinz/saml_xml_update_bug
Fixed #17089: SAML metadata now updating with new XML uploads
2025-06-23 16:40:35 +01:00
snipe
7b04e30964 Merge pull request #17280 from grokability/fixes/#7246-manager-id-in-import
Fixed #7246 - added manager employee/username number to importer
2025-06-23 16:07:14 +01:00
snipe
6794f5e783 Added logging for manager import
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 15:46:04 +01:00
snipe
6e41ceff39 Fixed parameter order
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 15:41:56 +01:00
snipe
7ab47ff0de Fixed #7246 - added manager employee number to importer
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 15:11:57 +01:00
snipe
92d24d8702 Merge pull request #17277 from grokability/#17264-add-notes-to-bulk
Fixed #17264: add notes to bulk asset edit
2025-06-23 12:36:50 +01:00
snipe
bcbfd46682 Update controller
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 12:32:33 +01:00
snipe
bfd96a695f Add notes field and nulling checkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 12:32:28 +01:00
snipe
f27e8534dc Make sure $item exists
Signed-off-by: snipe <snipe@snipe.net>
2025-06-23 12:32:15 +01:00
snipe
040cd7ddbf Updated string to use shorter version
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 20:56:37 +01:00
snipe
8d6b21a076 Merge pull request #17241 from spencerrlongg/bug/17239-possible-500-when-checking-in-license-assigned-to-a-soft-deleted-user
Fixed #17239 - Add `withTrashed` User Search On License Checkin
2025-06-22 20:14:03 +01:00
snipe
2d36b25017 Merge pull request #17258 from Robert-Azelis/patch-13
Company info - user print assets
2025-06-22 20:13:24 +01:00
snipe
1f9e4306ae Fixed #17261 - check for either old or new label engine QR
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 20:07:30 +01:00
snipe
5242e0b36e Merge pull request #17270 from grokability/fixes/#17249-order-by-location-in-maintenances
Fixed #17249 - sort by location in asset maintenances
2025-06-22 19:41:14 +01:00
snipe
e50505532e Fixes #17249 - sort by location in asset maintenances
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:38:39 +01:00
snipe
f05ef18d55 Merge pull request #17269 from grokability/fixes/#17256-user-eula-view
Fixed #17256: fixed permissions for non-super-admins to view their own EULAs
2025-06-22 19:29:41 +01:00
snipe
f6eccd7277 Added profile routes
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:25:30 +01:00
snipe
4d1258c64b Fixed downloadformatter
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:25:17 +01:00
snipe
103cbfd038 Use profile eula API view
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:25:05 +01:00
snipe
47069ad3f4 Added stored eula method on profile
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:24:45 +01:00
snipe
317f620992 Added profile controller
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:24:19 +01:00
snipe
8f43694582 Added eula download with user check in profile controller
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:24:06 +01:00
snipe
df30076ffd Update gate for user
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 19:23:50 +01:00
snipe
f81750617e Fixed mismatched html header tag
Signed-off-by: snipe <snipe@snipe.net>
2025-06-22 17:30:13 +01:00
Robert-Azelis
e4534c4319 Company info - user print assets 2025-06-20 10:33:09 +02:00
snipe
d6c09aae6b Updated paveit command to delete by user_id instead of ID
Signed-off-by: snipe <snipe@snipe.net>
2025-06-18 21:15:04 +01:00
spencerrlongg
d3d5230d0c add with trashed to user search 2025-06-17 19:15:13 -05:00
Marcus Moore
6f3c5c44a5 Remove assertion 2025-06-17 14:31:14 -07:00
Marcus Moore
67b32ca14d Mail content improvements 2025-06-17 14:22:54 -07:00
Marcus Moore
bffaf477ea Method order 2025-06-17 14:22:44 -07:00
Marcus Moore
cba45ece12 Add image 2025-06-17 13:57:40 -07:00
Marcus Moore
3290d7f401 Add translations 2025-06-17 13:27:05 -07:00
Marcus Moore
ef3827376d Add todo 2025-06-17 13:17:42 -07:00
Marcus Moore
4ae8a91051 Remove unused method 2025-06-17 12:34:43 -07:00
Marcus Moore
ff4819ac68 Get licenses working 2025-06-17 12:34:16 -07:00
Marcus Moore
58af133853 Re-add some test cases 2025-06-17 11:51:33 -07:00
Godfrey M
8199cd2118 comment merge methods for now 2025-06-17 10:59:57 -07:00
snipe
9f02b80cf1 Merge pull request #17233 from grokability/fixes-#17232-duplicate-expected-checkin-field
Fixed #17232 - removed duplicate expected_checkin field on asset edit/create
2025-06-17 18:14:36 +01:00
snipe
d3e4e81168 Fixed #17232 - removed duplicate expected_checkin field
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 18:08:21 +01:00
snipe
38195c0a8f Merge pull request #17225 from grokability/fixes-ui-issues-in-manager-view
Fixes #17227 and #17228 - UI issues in manager view
2025-06-17 17:28:52 +01:00
snipe
5fa11e4278 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 16:57:26 +01:00
snipe
c39b52fcb5 Move select list closer to the table, removed awkward icon
This makes the view more consistent with the normal layout and removes the CSRF that was being passed in the GET on the form.submit

Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 16:57:17 +01:00
snipe
ec65fc1e65 Move manager view option to misc section
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 16:56:56 +01:00
snipe
32d8646d96 Add @lukaskraic as a contributor 2025-06-17 16:26:17 +01:00
snipe
e8835fc2b1 Merge pull request #17096 from lukaskraic/feature/manager-view-v2
Manager View Feature
2025-06-17 16:24:40 +01:00
snipe
054a06c5dc Update resources/lang/en-US/admin/settings/general.php
Co-authored-by: Marcus Moore <contact@marcusmoore.io>
2025-06-17 15:39:26 +01:00
snipe
9c61d2eb22 Removed common elements in tables
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 14:25:02 +01:00
snipe
d66b6cfee6 Merge pull request #17220 from grokability/added-advanced-search-to-activity-log
Added advanced search to activity log
2025-06-17 13:24:31 +01:00
snipe
89c0427b2f Added fullscreen option
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 13:24:02 +01:00
snipe
3fec10d447 Added advanced search to activity log
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 13:20:39 +01:00
snipe
f8b4981bfe Merge pull request #17219 from grokability/fixes-#17213-search-by-admin
Fixes #17213:  search by admin (created_by) in activity report
2025-06-17 13:02:51 +01:00
snipe
130669a2f9 Added adminuser relationship in Searchable to concat first name and last name
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 13:01:57 +01:00
snipe
c2c79ee231 Make created_by searchable
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 12:56:39 +01:00
snipe
86f10bd702 Added employee number to searchable relation
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 12:56:30 +01:00
snipe
b496a06fc0 Merge pull request #17218 from grokability/fixed-#17212-hide-columns-on-print
Fixed #17212 - hide columns from print view
2025-06-17 12:37:43 +01:00
snipe
f865a6cb37 Added 'printIgnore' => true, to presenters to hide checkbox and action column from print view
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 12:35:05 +01:00
snipe
89186ea4f8 Merge pull request #17195 from grokability/#17192-fixes-wonky-layout-on-bulk-edit
Fixed #17192 - wonky layout on bulk edit screens
2025-06-17 11:55:31 +01:00
snipe
fb19985186 Use the same variable for fieldset closing tag
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 11:52:47 +01:00
snipe
ebc6e1221a Merge pull request #17198 from akemidx/ghissue-16241
Fixed #16241 - menu items not getting class="active" on click.
2025-06-17 11:40:56 +01:00
snipe
2b91dcb700 Merge pull request #17216 from grokability/#17215-wrong-logo-check-in-print
Fixed #17215: Check for acceptance logo vs regular logo
2025-06-17 11:38:19 +01:00
snipe
9d2e333fd6 Check for acceptance logo vs regular logo
Signed-off-by: snipe <snipe@snipe.net>
2025-06-17 11:36:10 +01:00
Godfrey M
013ad1069c saving certs sooner in the stack 2025-06-16 16:30:00 -07:00
Godfrey M
ec059717f6 adds id to XML textbox, now updates 2025-06-16 16:01:11 -07:00
akemidx
418566db3f fixes the easy ones 2025-06-16 18:41:36 -04:00
snipe
7be9463be6 Merge pull request #17180 from grokability/#12653-added-jobtitle-to-asset-listing
Fixed #12653 - added jobtitle to asset listing
2025-06-16 20:29:01 +01:00
snipe
51712bc7d6 Check for whether any of the models have a fieldset
Signed-off-by: snipe <snipe@snipe.net>
2025-06-16 16:40:07 +01:00
snipe
7b889d22d2 Fixed HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-06-16 16:14:01 +01:00
snipe
e8aad989ec Updated translations
Signed-off-by: snipe <snipe@snipe.net>
2025-06-16 14:27:47 +01:00
snipe
4006d64d60 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-06-16 14:25:59 +01:00
snipe
d792d99375 Removed stray space for favicon
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 03:51:50 +01:00
snipe
b11036a2e5 Fixed #11977 - changed path in backups language
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 03:40:54 +01:00
snipe
01de69a250 Merge pull request #17181 from grokability/fixed-#14542-missing-fullscreen-in-locations
Fixed #14542 - added fullscreen option for location view tabs
2025-06-15 02:57:51 +01:00
snipe
5e1c2e7feb Fixed #14542 - added fullscreen option for location view tabs
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 02:56:07 +01:00
snipe
b842aa11e5 Remove debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 02:25:25 +01:00
snipe
ff01078b60 Fixed #12653 - adds job title to asset listing
Signed-off-by: snipe <snipe@snipe.net>
2025-06-15 02:23:53 +01:00
snipe
b1e92293fc Merge pull request #14998 from timoschwarzer/feature/department-manager-in-table
Added #14997: Display department manager in user view and list
2025-06-14 23:51:41 +01:00
snipe
443f69bd82 Merge branch 'develop' into feature/department-manager-in-table 2025-06-14 23:48:52 +01:00
snipe
f4decbf52e Fixed #15480 - adds location to acceptance email
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 23:08:37 +01:00
snipe
bd2c311e4f One more fix to bulk menu
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 22:22:42 +01:00
snipe
2dcab6d0b3 Fixed logic on bulk menu
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 22:21:00 +01:00
snipe
c68a97198f Fixes #8212 - force date format for purchase date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 20:40:15 +01:00
snipe
2702c3da2b Merge pull request #17178 from grokability/fixes-maintenance-name-on-delete
Fixed #17177 - use maintenance title for delete confirmation
2025-06-14 17:44:21 +01:00
snipe
da06e9afd5 Fixes #17177 - undefined title on asset maintenance
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 17:40:14 +01:00
snipe
1623f13539 Merge pull request #17147 from chrisnovakovic/php_upload_limit-hardening
Docker: harden updating of `php.ini` in entrypoint
2025-06-14 17:11:15 +01:00
snipe
5910982a4f Merge pull request #17176 from grokability/nicer-bulk-options-for-assets
Add logic around menu options
2025-06-14 17:06:49 +01:00
snipe
74630b36b0 Merge pull request #17175 from grokability/fixes/17172-checkin-date
Fixed #17172 - Better handle checkin date overrides
2025-06-14 17:06:30 +01:00
snipe
ace4a5d614 Add logic around menu options
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 17:02:09 +01:00
snipe
0d41947f64 Better handle checkin date overrides
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:54:25 +01:00
snipe
78de3b3591 Merge pull request #17174 from grokability/fixes-17163-permissions-on-maintenances
Fixed #17163 - insufficient permissions on editing an asset maintenance
2025-06-14 16:43:27 +01:00
snipe
0a4a6e7ba3 Removed unusued relationship
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:37:44 +01:00
snipe
090399b336 Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:34:12 +01:00
snipe
47afb15970 Updated HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:24:05 +01:00
snipe
b1ba3376aa Validation for completion date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 16:13:57 +01:00
snipe
8c1e19e77c Fixed breadcrumbs
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 15:53:05 +01:00
snipe
0801d1473c Use forem group div
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:57:55 +01:00
snipe
6d98878c72 Removed redirect
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:31:15 +01:00
snipe
2d404fdadc Fixed breadcrumbs
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:30:52 +01:00
snipe
b264fde165 Show title if it’s an edit
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:30:43 +01:00
snipe
970ff25e5e Added model and company to manufacturer view
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:30:27 +01:00
snipe
9dd3eee65c Pull incorrect check for company scoping
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:04:09 +01:00
snipe
957faa6651 Fixed datepicker prepopulating with start_date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 14:03:52 +01:00
snipe
cc7dcc6e81 Disallow multiple on editing a maintenance
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 13:39:18 +01:00
snipe
f666cba104 Fixed RB-3991 - override route for non-existent fieldset
Signed-off-by: snipe <snipe@snipe.net>
2025-06-14 12:17:31 +01:00
Marcus Moore
7f35498919 Remove some test cases 2025-06-12 15:56:24 -07:00
Marcus Moore
297205ff91 Tests 2025-06-12 15:55:25 -07:00
Marcus Moore
6f99381d13 Handle accessories 2025-06-12 15:49:38 -07:00
Marcus Moore
45a42b00ad Add test 2025-06-12 15:48:48 -07:00
Marcus Moore
6586858284 Scaffold some test cases 2025-06-12 15:31:32 -07:00
Marcus Moore
19cce15e54 Revert "Method signature change"
This reverts commit a60ffc0702.
2025-06-12 13:54:53 -07:00
Marcus Moore
62b8e4c46f Revert "Inline some values"
This reverts commit 74b7d27408.
2025-06-12 13:54:46 -07:00
Marcus Moore
74b7d27408 Inline some values 2025-06-12 13:49:19 -07:00
Marcus Moore
a60ffc0702 Method signature change 2025-06-12 13:46:05 -07:00
snipe
9b8524ba27 Merge pull request #17109 from akemidx/bug/sc-28718-2
FIXED: Adding Total to Consumable View Page
2025-06-12 21:44:08 +01:00
Marcus Moore
32526d77b8 Add todo 2025-06-12 13:35:22 -07:00
snipe
ba7db8f7b3 Merge pull request #17092 from Godmartinz/fix-bulk_checkout_undeployable
Adds `hasUndeplyableStatus` check to bulk checkout
2025-06-12 21:29:26 +01:00
snipe
04712ad252 Merge pull request #17151 from Godmartinz/bulk-checkout-add-pending-status
Fixed #17028: Allows bulk editing assets with a status type of pending
2025-06-12 21:27:44 +01:00
Marcus Moore
5711a9e148 Fix test 2025-06-12 13:02:26 -07:00
Marcus Moore
a0f40c2dfb Tests 2025-06-12 12:56:24 -07:00
Marcus Moore
34636016eb Improve test 2025-06-12 12:47:03 -07:00
Marcus Moore
95027e329c Formatting 2025-06-12 12:44:29 -07:00
Marcus Moore
c9778a73c7 Wire up category controller 2025-06-12 12:32:34 -07:00
Marcus Moore
628d2a0a0a Flesh out mail contents 2025-06-12 12:12:38 -07:00
Marcus Moore
cc0ff1ec1f Handle missing recipient 2025-06-12 11:53:30 -07:00
snipe
6543540509 Merge pull request #17166 from Godmartinz/item_name_fix_from_account_eula_table
FIXED: #17136 loads item relation properly in account EULA tab
2025-06-12 11:53:51 +01:00
Marcus Moore
cd53fc6318 Scaffold email contents 2025-06-11 16:56:40 -07:00
Godfrey M
4934b6c4da fix query for item relationship to load properly 2025-06-11 11:42:16 -07:00
snipe
59db38524b Merge pull request #17094 from spencerrlongg/snipe-it-17051
Fixes #17051 - Nulling Custom Fields In Bulk Asset Updates
2025-06-11 10:31:28 +01:00
snipe
3f5cfc3a4b Merge pull request #17142 from marcusmoore/fixes/present-on-null-in-expected-checkins-notification
Fixed Expected checkin notification erroring on unknown users
2025-06-11 10:27:49 +01:00
snipe
3443f02c0a Merge pull request #17141 from marcusmoore/fixes/undefined-manages_users_count-on-user
Fixed bad method calls in user index api call
2025-06-11 10:26:13 +01:00
snipe
9a012ca01e Merge pull request #17154 from Robert-Azelis/patch-10
Update user print assigned assets
2025-06-11 10:24:50 +01:00
snipe
509ef34cca Merge pull request #17155 from Robert-Azelis/patch-11
Update location print assigned assets
2025-06-11 10:24:29 +01:00
snipe
49c289a094 Merge pull request #17158 from grokability/changes-default-history-column-visibility
Changed default visibility on history views
2025-06-11 10:19:33 +01:00
snipe
10e5d88fb6 Changed default visibility on history views
Signed-off-by: snipe <snipe@snipe.net>
2025-06-11 10:16:26 +01:00
snipe
ae64fb3fdb Merge pull request #17157 from grokability/fixed-#17138-category-type-case-sensitive
Fixed #17138 - category type was case-sensitive
2025-06-11 10:12:05 +01:00
snipe
cac2fde504 Fixed #17138 - category type in category importer is case sensitive
Signed-off-by: snipe <snipe@snipe.net>
2025-06-11 10:08:59 +01:00
snipe
bb38a96fd1 Merge pull request #17152 from marcusmoore/fixes/handle-category-missing-upon-checkin
Handle potentially missing category upon checkin
2025-06-11 09:36:50 +01:00
Robert-Azelis
79dbcb10c9 Update location print assigned assets
Lets allow user to select print layout: portrait or landscape
Use dedicated logo for PDF documents: acceptance_pdf_logo
2025-06-11 09:22:13 +02:00
Robert-Azelis
7c80fdea58 Update user print assigned assets
Lets allow user to select print layout: portrait or landscape
Use dedicated logo for PDF documents: acceptance_pdf_logo
2025-06-11 09:19:51 +02:00
Godfrey M
5500a42744 change message from error to warning 2025-06-10 15:10:10 -07:00
Godfrey M
ae46264707 removed comment 2025-06-10 15:07:42 -07:00
Godfrey M
f2d8665e54 adds tests 2025-06-10 15:07:04 -07:00
Marcus Moore
db7110d6b2 Remove test 2025-06-10 10:43:23 -07:00
Marcus Moore
4e06b597fe Handle category missing in Consumable 2025-06-10 10:43:02 -07:00
Godfrey M
fe006d05d3 allows to change the status to pending in bulk edit while deployed 2025-06-10 10:41:26 -07:00
Marcus Moore
358b70e280 Handle category missing in Accessory 2025-06-10 10:36:59 -07:00
Marcus Moore
a060dde625 Add failing test 2025-06-10 10:34:25 -07:00
Godfrey M
3cfed72af4 removes undeployables from asset_id array 2025-06-10 10:11:00 -07:00
Chris Novakovic
4c59989236 Docker: harden updating of php.ini in entrypoint
The Docker entrypoint scripts set values for the `upload_max_filesize`
and `post_max_size` directives in `php.ini` based on the value of the
`PHP_UPLOAD_LIMIT` environment variable, subject to the following
restrictions:

* Exactly one file matches `/etc/php/*/apache2/php.ini` (on Ubuntu) or
  `/etc/php*/php.ini` (on Alpine) - if, for example, more than one PHP
  package is installed in the base image, `PHP_UPLOAD_LIMIT` will not be
  honoured.
* The `php.ini` file already sets a non-default value for the
  `upload_max_filesize` or `post_max_size` directives - this is
  currently the case for the configurations inherited from upstream, but
  is not guaranteed. If the default values are relied upon,
  `PHP_UPLOAD_LIMIT` will silently not be honoured (although the script
  output will claim that it is).

Iterate over the lines outputted by `file(1)` so `PHP_UPLOAD_LIMIT` is
honoured in all available `php.ini` files, and set `upload_max_filesize`
and `post_max_size` regardless of whether they already have a value set.
2025-06-10 13:39:46 +01:00
Marcus Moore
6c1adff5c8 Extract translation string 2025-06-09 15:33:39 -07:00
Marcus Moore
9293bdca06 Don't render link for Unknown User 2025-06-09 15:32:07 -07:00
Marcus Moore
beeccbfb44 Handle unknown users gracefully 2025-06-09 15:30:03 -07:00
Marcus Moore
0d3d2e2e78 Fix keyes 2025-06-09 12:55:38 -07:00
Marcus Moore
2af7605451 Add failing tests 2025-06-09 12:55:09 -07:00
snipe
976cc1c86f Fixed second incorrect string
Signed-off-by: snipe <snipe@snipe.net>
2025-06-09 14:00:47 +01:00
snipe
cbbf3aa6c8 Fixed incorrect translation string
Signed-off-by: snipe <snipe@snipe.net>
2025-06-09 13:40:34 +01:00
snipe
4f8ff98d5b Merge pull request #17129 from grokability/fixes#17127-added-note-to-eula-api
Fixed #17127 - added note to EULA info
2025-06-08 15:29:47 +01:00
snipe
d4fe81c290 Fixed #17127 - added note to EULA info
Signed-off-by: snipe <snipe@snipe.net>
2025-06-08 15:27:50 +01:00
snipe
cbdf03aa66 Nicer code formatting
Signed-off-by: snipe <snipe@snipe.net>
2025-06-08 15:18:59 +01:00
snipe
d756670c56 Check for supplier before trying to show
Signed-off-by: snipe <snipe@snipe.net>
2025-06-08 15:17:24 +01:00
snipe
9ef7b0e64a Fixed missing translation string
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 20:41:36 +01:00
snipe
2d7c0f7e5f Merge pull request #16469 from grokability/fix_action_date
Fixed `action_date` in `action_logs`
2025-06-06 17:18:39 +01:00
snipe
ec8ddc197f Merge branch 'develop' into fix_action_date 2025-06-06 17:18:27 +01:00
snipe
b48e56bd46 Merge pull request #17110 from Godmartinz/allow-users-to-dl-eula-from-account
Fixed #17084 - Adds Eula table to User account area with download option
2025-06-06 17:07:19 +01:00
snipe
753ca93371 Merge pull request #17126 from grokability/fixes-#17023-multi-maintenances
Fixes #17023 - added ability to bulk add maintenances
2025-06-06 17:01:50 +01:00
snipe
04f71e7f6a Shorter syntax
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 16:58:10 +01:00
snipe
5d129dd420 Small form fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 16:56:45 +01:00
snipe
1c37c630aa Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 16:26:37 +01:00
snipe
d329d6104e Make supplier_id nullable
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:55:54 +01:00
snipe
482723f3bc Updated blade to use multiple
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:47:07 +01:00
snipe
cbc025b1ff Updated save method to save multiple
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:46:55 +01:00
snipe
bf8ceceabe Add bulk maintenance handler
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:42:14 +01:00
snipe
fba4bba132 Make supplier not required anymore
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:41:41 +01:00
snipe
0ded40c037 Add multiselect to maintenance form
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:41:30 +01:00
snipe
6f486a37ff Added maintenance option to bulk menu
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 15:41:02 +01:00
snipe
1ef822997b Merge pull request #17125 from grokability/fixed-17117-added-translation
Fixed #17117 - use translation for “site default”
2025-06-06 14:04:08 +01:00
snipe
ea66629e98 Fixed #17117 - use translation for “site default”
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 14:03:17 +01:00
snipe
84c9979fe3 Merge pull request #17124 from grokability/add-print-button-to-tables
Added print button to tables
2025-06-06 13:52:22 +01:00
snipe
867a992183 Added print button to tables
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 13:51:12 +01:00
snipe
c3ad7d649c Merge pull request #17122 from grokability/added_highlighting_to_search
Added highlighting on table search
2025-06-06 12:47:31 +01:00
snipe
fc250e228d Added highlighting on table search
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 12:43:47 +01:00
snipe
37e81568ea Merge pull request #17120 from grokability/fixes-11807-datepicker-blade-component
Fixed #11807 - Standardize date-picker
2025-06-06 11:44:54 +01:00
snipe
048d910d5b Use partial
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:33:17 +01:00
snipe
b162aba445 Added localization for datepicker
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:33:02 +01:00
snipe
974627849b Added datepicker partial
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:32:47 +01:00
snipe
e18e9f699e Use blade component in datepicker partial
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:32:37 +01:00
snipe
cc9209d2de Removed EOL date and purchase date partials
Signed-off-by: snipe <snipe@snipe.net>
2025-06-06 11:32:13 +01:00
snipe
dd3d264e63 Merge pull request #16675 from Godmartinz/null_location_id_fix
Adds a check for assigned target not being null when creating an asset
2025-06-06 08:30:31 +01:00
snipe
246f1373a8 Merge pull request #16948 from marcusmoore/bug/sc-29181
Require assigned_x to be integer on asset model
2025-06-06 07:57:20 +01:00
snipe
b993f4270e Merge pull request #17114 from grokability/fixes-16934-update-asset-by-id
Fixed #16934 and #17068 - update asset by ID in importer
2025-06-06 07:55:03 +01:00
snipe
6c51ef11b1 Merge pull request #17115 from marcusmoore/17067-limit-cc-if-acceptance-required
Fixed #17067: Allow only sending cc email when acceptance required
2025-06-06 07:41:26 +01:00
Marcus Moore
ae98f6276e Replace reject with declined 2025-06-05 17:24:57 -07:00
Marcus Moore
7424a5987b Add subject 2025-06-05 17:22:31 -07:00
Marcus Moore
2d08749207 Improve readability 2025-06-05 16:56:11 -07:00
Marcus Moore
db50e98ae3 Populate tests 2025-06-05 16:51:44 -07:00
Marcus Moore
7ec0925c69 Scaffold out tests 2025-06-05 16:44:44 -07:00
Marcus Moore
df1361aa43 Scaffold test 2025-06-05 16:42:31 -07:00
akemidx
6db04c86df making fields active. first pass 2025-06-05 18:46:33 -04:00
Marcus Moore
bec80b443c WIP: begin to send email 2025-06-05 15:15:22 -07:00
Marcus Moore
333501fe55 WIP: create mail class 2025-06-05 15:07:19 -07:00
Marcus Moore
063553d4f7 Scaffold scenarios 2025-06-05 14:57:15 -07:00
Marcus Moore
19b9e50281 Update livewire component for alert_on_response 2025-06-05 14:40:19 -07:00
Marcus Moore
2a68b4aeff Update test for updating category with alert_on_response 2025-06-05 14:36:06 -07:00
Marcus Moore
5e25150521 Add another test case 2025-06-05 14:26:56 -07:00
Marcus Moore
cb183d3645 Store alert_on_response_id on CheckoutAcceptance 2025-06-05 14:06:22 -07:00
Marcus Moore
96bce301a0 Add alert_on_response to Category 2025-06-05 13:43:20 -07:00
Marcus Moore
360f5b7538 Add alert_on_response_id to CheckoutAcceptance 2025-06-05 13:10:18 -07:00
Marcus Moore
77234f6580 Extract translation strings 2025-06-05 12:24:46 -07:00
Marcus Moore
088e6af0b5 Remove admin_cc_email validation for admin_cc_always 2025-06-05 12:07:14 -07:00
Lukas Kraic
16fb1018a2 List users code refactoring 2025-06-05 20:05:38 +02:00
Godfrey M
e2e54677ee add auth to api call, gave more specificity to the relationship 2025-06-05 10:59:16 -07:00
Lukas Kraic
ad6fe855a9 Fix code style: Change comments 2025-06-05 18:53:36 +02:00
Lukas Kraic
c50c97d149 Fix code style: Change comments 2025-06-05 18:18:07 +02:00
Lukas Kraic
8b98ae15f0 Fix code style: Add comment 2025-06-05 18:08:34 +02:00
Lukas Kraic
261f84d5f5 Fix code style: Remove comment 2025-06-05 17:55:46 +02:00
Lukas Kraic
29989ac24e Fix code style: Remove empty line after end comment 2025-06-05 13:50:57 +02:00
snipe
7a93e94fa6 Add ID to field list
Signed-off-by: snipe <snipe@snipe.net>
2025-06-05 12:35:30 +01:00
snipe
e33f73fe9f Fixed comment text
Signed-off-by: snipe <snipe@snipe.net>
2025-06-05 11:56:14 +01:00
snipe
6291389df5 Fixed #16934 - update asset by ID in importer
Signed-off-by: snipe <snipe@snipe.net>
2025-06-05 11:53:57 +01:00
snipe
ed817dc414 Merge branch 'develop' into snipe-it-17051 2025-06-05 11:37:17 +01:00
Lukas Kraic
7494fa6bc9 Fix code style: Remove unnecessary end comment from short conditional block 2025-06-05 12:03:42 +02:00
Lukas Kraic
62e50dbe52 Fix code style: Add semantically correct end comments 2025-06-05 11:56:13 +02:00
Lukas Kraic
30c090ba2d Fix code style: Add descriptive end comment for conditional block 2025-06-05 11:45:05 +02:00
Lukas Kraic
7ff82e6043 Delete comments 2025-06-05 11:07:42 +02:00
snipe
2950fb1041 Merge pull request #17108 from Godmartinz/clear-buttons-for-custom-fields
Adds a clear button to custom radio buttons in bulk edit
2025-06-05 10:03:27 +01:00
Lukas Kraic
61d3e2fb49 Fix code style: Add required whitespace in end block comments 2025-06-05 09:23:58 +02:00
Lukas Kraic
fb18c1a0be Fix code style: Remove whitespace in end block comments 2025-06-05 08:59:07 +02:00
Godfrey M
68c082e0dc fix doc blocks 2025-06-04 15:48:35 -07:00
Godfrey M
ee3deb9c63 made eula api route, formatted table, cleaned up code 2025-06-04 15:04:59 -07:00
Marcus Moore
c1505de8d6 Update wording 2025-06-04 13:02:04 -07:00
Marcus Moore
10be434c13 Populate tests 2025-06-04 12:58:49 -07:00
Marcus Moore
92e22eead5 Formatting 2025-06-04 12:51:59 -07:00
Marcus Moore
31db86abd3 Simplify test 2025-06-04 12:51:48 -07:00
Marcus Moore
8e70ff135a Scaffold tests 2025-06-04 12:44:55 -07:00
akemidx
5ec52f7471 beginning of everything. tried some stuff but yea. something is up 2025-06-04 15:10:16 -04:00
Marcus Moore
8bc57f98a5 Split test case 2025-06-04 11:51:29 -07:00
Godfrey M
6f4cee6334 adds Eula tab and count to user account 2025-06-04 11:38:50 -07:00
Godfrey M
6beccc5e60 adds clear ability to custom radio buttons 2025-06-04 10:24:45 -07:00
Godfrey M
fed8e10644 Merge remote-tracking branch 'upstream/develop' into clear-buttons-for-custom-fields 2025-06-04 10:06:59 -07:00
snipe
3c0121c1d0 Merge pull request #17107 from grokability/fixes-16218-add-centos
Fixed #16218 - added centos to the switch case
2025-06-04 15:33:23 +01:00
snipe
02021e3fb9 Fixed #16218 - added centos to the switch case
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 15:27:19 +01:00
Lukas Kraic
ea447365fa Fix Codacy warnings II 2025-06-04 15:40:17 +02:00
Lukas Kraic
60989d6766 Fix Codacy warnings 2025-06-04 15:17:55 +02:00
snipe
a2960dc653 Merge pull request #17103 from grokability/fixes-17102-add-cc-as-search-string-in-settings
Fixed #17102 - added keywords to admin settings search for notifications
2025-06-04 12:12:17 +01:00
snipe
702499dd79 Fixed #17102 - added keywords to admin settings search for notifications
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 12:09:57 +01:00
snipe
482c427e34 Merge pull request #17100 from uberbrady/add_deleted_at_index_to_action_logs
Fixed #16205 - Add an index to deleted_at for those with many deleted action_logs
2025-06-04 11:48:45 +01:00
snipe
67910490bd Merge pull request #17101 from grokability/fixes-16157-added-advanced-search-to
Fixed #16157 - Added advanced search to users
2025-06-04 11:48:24 +01:00
snipe
fdb5ab2293 Added advanced search to users
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 11:46:38 +01:00
Brady Wetherington
092d9d1e42 Add deleted_at index to action_logs for people with many deleted action_logs 2025-06-04 11:23:09 +01:00
snipe
e657f11531 Merge pull request #17099 from grokability/fixes-16240-localization-strings
Fixed #16240 - made additional strings translatable
2025-06-04 10:40:21 +01:00
snipe
9aac183318 Added aria tag for accessibility
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 10:40:05 +01:00
snipe
c8c2867305 Remove unused translation
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 10:39:08 +01:00
snipe
3e1f71026c Fixed #16240 - made additional strings translatable
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 10:37:04 +01:00
snipe
dc562d8c20 Remove error log
Signed-off-by: snipe <snipe@snipe.net>
2025-06-04 08:43:40 +01:00
Lukas Kraic
84ec5aea26 Manager View Feature 2025-06-04 09:07:26 +02:00
Marcus Moore
444c13c6ea Scaffold template 2025-06-03 17:10:15 -07:00
spencerrlongg
12d5e4f7d2 cleanup and test 2025-06-03 19:07:02 -05:00
spencerrlongg
0fb1639915 this works! 2025-06-03 18:06:41 -05:00
Marcus Moore
d01f7cf317 Adhere to admin_cc_always setting 2025-06-03 15:32:11 -07:00
spencerrlongg
03725c8e0c custom field null and filtering 2025-06-03 17:21:07 -05:00
Marcus Moore
3942489d21 Add Test suffix and scaffold test 2025-06-03 14:13:52 -07:00
Marcus Moore
51479c8bbc Scaffold failing test 2025-06-03 13:28:40 -07:00
Marcus Moore
ea3364ab68 Split test case 2025-06-03 13:19:44 -07:00
Godfrey M
cb608d7fd1 testing buttons out 2025-06-03 11:58:57 -07:00
Godfrey M
7129008428 remove testing changes 2025-06-03 11:44:38 -07:00
Godfrey M
3b832f507f fixes status check for bulk checkout 2025-06-03 11:38:59 -07:00
Godfrey M
b5849500f9 add isDeplyable check 2025-06-03 10:49:17 -07:00
snipe
9eaabf95a0 Make version number match tagged version :(
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 11:02:41 +01:00
snipe
724f38abc2 Merge pull request #16998 from kovacs-andras/develop
Bumped container image versions
2025-06-03 06:22:08 +01:00
snipe
0dfc083a91 Removed log error
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 06:07:06 +01:00
snipe
16432f503a Merge pull request #17087 from grokability/#17085-checkin-and-delete-not-nulling-assigned_type
Fixed #17085 - assigned_type not being nulled on asset delete+checkin
2025-06-03 05:53:24 +01:00
snipe
7c9433be5d Added migration to fix existing wonky data
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:37:37 +01:00
snipe
e4ce71ff14 Added time on action date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:30:53 +01:00
snipe
45c6406ff4 Added console command for fixup
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:30:43 +01:00
snipe
550e2b6bb8 Null both assigned to and assigned type on delete
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:30:30 +01:00
snipe
a7bb890729 Removed action date from array of things to log
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:29:49 +01:00
snipe
3d8f8faf01 Added action_date
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:28:58 +01:00
snipe
55cf5877c4 Updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 05:28:45 +01:00
Marcus Moore
dec3c4aff3 Improve wording 2025-06-02 17:45:08 -07:00
Marcus Moore
79e00c1191 Require admin_cc_email if admin_cc_always is true 2025-06-02 17:21:58 -07:00
Marcus Moore
12dc33244d Start storing admin_cc_always 2025-06-02 17:20:01 -07:00
Marcus Moore
6e37f945ac Add test helpers 2025-06-02 17:13:37 -07:00
Marcus Moore
d75120000a Add failing tests 2025-06-02 17:11:18 -07:00
Marcus Moore
9e4aab7165 Scaffold tests 2025-06-02 17:05:18 -07:00
Marcus Moore
6bc3209333 Use @checked for inputs 2025-06-02 17:05:00 -07:00
Marcus Moore
054ff42547 Add migration for admin_cc_always 2025-06-02 17:03:14 -07:00
spencerrlongg
11b47b308b front end done, sloppy 2025-06-02 18:39:08 -05:00
Marcus Moore
367ab8ddd5 Add help text 2025-06-02 16:27:04 -07:00
Marcus Moore
4f5d4a0984 Scaffold settings page changes 2025-06-02 16:25:06 -07:00
snipe
40489c53d6 Merge pull request #17078 from grokability/fixes-#17076-validation-on-bulk-submit
Fixed #17076 - Disable optional status ID form field if value is blank/Do Not Change
2025-06-02 22:28:31 +01:00
snipe
93b760d53b Disable form fields if the value is blank
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 22:16:36 +01:00
snipe
e86996bc7e Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 18:15:21 +01:00
snipe
14244f45b6 Duplicates PR #16957
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 18:13:16 +01:00
snipe
1b9d90a322 Added over sixty test
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 17:36:50 +01:00
snipe
736f74d083 Merge pull request #17074 from uberbrady/improve_api_rate_limiting
Fix to rate-limiter on higher rate-limits
2025-06-02 17:25:02 +01:00
Brady Wetherington
8194c6efdb Fix to rate-limiter on higher rate-limits 2025-06-02 17:21:14 +01:00
snipe
aae2a17ad1 Add @amedranogil as a contributor 2025-06-02 15:27:55 +01:00
snipe
f44150668c Merge pull request #17038 from amedranogil/develop
more robust php.ini update.
2025-06-02 15:27:28 +01:00
snipe
6eed2deb09 Merge pull request #17013 from Robert-Azelis/patch-9
API Models - added requestable for API request
2025-06-02 15:26:08 +01:00
snipe
878c6e7031 Merge pull request #17019 from grokability/#15320-status_to_bulk_checkout
Fixed #15320 - added status label to bulk checkout
2025-06-02 15:05:44 +01:00
snipe
1f4a73fab6 Merge pull request #17062 from grokability/add_category_importer
Added category importer
2025-06-02 15:03:31 +01:00
snipe
7a315523fe Improved CSV
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 15:01:16 +01:00
snipe
6f082e662b Fixed weird layout
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 14:52:41 +01:00
snipe
018c981c5a Merge pull request #17042 from marcusmoore/chore/replace-customfield-elements-macro-take-two
Replace customfield_elements form macro take two
2025-06-02 10:18:19 +01:00
snipe
0149773a03 Fixed variable name
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 04:14:51 +01:00
snipe
5d46d90725 Added category importer
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 04:11:54 +01:00
snipe
0544e05f32 Merge pull request #17061 from grokability/add_manufacturer_importer
Added manufacturer importer
2025-06-02 03:05:25 +01:00
snipe
80ff42a41f Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 03:01:56 +01:00
snipe
90b7df45b9 Added tests and support helper
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:55:11 +01:00
snipe
32858b973a Added sample CSV
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:55:00 +01:00
snipe
40ba8d0de1 Fixed “send welcome email” detection
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:54:34 +01:00
snipe
8ddbb4e64f Added manufacturer factory
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:54:21 +01:00
snipe
cc40c48aac Added manufacturers import fields
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:54:03 +01:00
snipe
522ab9e0f5 Added manufacturer importer
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:53:46 +01:00
snipe
97187aa7eb Skip manufacturers on checkout import type
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:53:37 +01:00
snipe
d93a5aa623 Added redirect after import
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:53:09 +01:00
snipe
a5b88982bf Added manufacturer icon
Signed-off-by: snipe <snipe@snipe.net>
2025-06-02 02:52:55 +01:00
snipe
df71bdcada Merge pull request #17044 from marcusmoore/bug/sc-29302
Handle missing location when rendering labels
2025-06-02 02:21:19 +01:00
snipe
51bab2dd26 Merge pull request #17045 from grokability/docker-laravel-log-permissions
Docker: Ensure permissions on Laravel log file
2025-05-30 06:52:31 +01:00
Jeremy Price
ed8da6ad1b Docker: Ensure permissions on Laravel log file
FIXES: https://github.com/grokability/snipe-it/issues/12725

In some of our Docker startups, it was possible for the Laravel log file
to be created with root permissions, causing future errors when the
non-root webapp tries to write to it.

We'll now always chown (and create, if necessary) the log file to the proper
user after running any artisan commands (as root)

We _could_ run them as the proper user via su, but IMO not doing so keeps the
script easier to read, but I'm not married to the approach. I'd still
want to keep the chown command(s) in, because it will also fix the
permissions for anyone who already has this issue.
2025-05-29 17:45:14 -07:00
Marcus Moore
18d0a04efc Avoid dumping pdf contents to test results 2025-05-29 15:05:08 -07:00
Marcus Moore
bb68ed3ad9 Handle asset not having location 2025-05-29 14:49:53 -07:00
Marcus Moore
402ca07aa2 Add failing test 2025-05-29 14:20:15 -07:00
snipe
28dc358df1 Merge pull request #17041 from grokability/improve_locations_and_supplier_api
Small refinements for suppliers and locations API and list view
2025-05-29 21:30:54 +01:00
Marcus Moore
3cf1e9d55d Remove customfield_elements macro 2025-05-29 13:30:23 -07:00
Marcus Moore
82b001ab5f Extract translation strings 2025-05-29 13:29:13 -07:00
Marcus Moore
7b272226ce Inline customfield_elements select 2025-05-29 13:29:13 -07:00
snipe
78d26fb7f6 Removed stray character
Typing is hard :(

Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 21:23:50 +01:00
snipe
930842e685 Removed unused method
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 21:18:19 +01:00
snipe
b938cb42d8 Merge pull request #17040 from marcusmoore/improve-acceptance-reminder-output
Avoid displaying empty table in `SendAcceptanceReminder` command
2025-05-29 21:14:09 +01:00
snipe
4c7b6d130f Added additional search and display fields for suppliers and locations
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 21:13:08 +01:00
Marcus Moore
af57ca4983 Avoid displaying empty table 2025-05-29 11:55:43 -07:00
snipe
40c31a1ad7 Eager load adminuser method
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:13:16 +01:00
snipe
7ae4a4177f Added created_by to transformer
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:13:05 +01:00
snipe
6efd323fbf Added adminuser method
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:12:48 +01:00
snipe
ed9dbcc777 Added created_by to location presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:12:33 +01:00
snipe
c2cf7de41b Use presenter for suppliers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 17:12:16 +01:00
Alejandro M. Medrano Gil
32bd14bd2d more robust php.ini update.
could solve #10830 when setting PHP_UPLOAD_LIMIT environment variable in docker command and/or docker-compose.
2025-05-29 17:46:23 +02:00
snipe
f9cbecdb17 Merge pull request #17037 from grokability/supplier_importer
Added #17036 - suppliers importer
2025-05-29 16:15:59 +01:00
snipe
7bb29a0277 Added sample import CSVs
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:08:17 +01:00
snipe
d5f7579e9f Added columns to suppliers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:08:01 +01:00
snipe
13fd43c45c Added tests and test support
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:07:51 +01:00
snipe
c08ce901cc Added strings
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:07:19 +01:00
snipe
94bd11d3c9 Added locations and supplier import types
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:07:11 +01:00
snipe
59c6e26b29 Fixed mapping
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 16:06:51 +01:00
snipe
bf7cc404f8 Set correct redirect
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 15:54:37 +01:00
snipe
12a2c71b90 Added icon
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 14:47:59 +01:00
snipe
6e2eeba0f6 Added supplier importer
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 14:47:54 +01:00
snipe
99a739fae3 Merge pull request #17035 from grokability/settings_style_improvements
Fixed #17034 - larger header color box on small views
2025-05-29 13:30:31 +01:00
snipe
0185f61c11 Fixed #17034 - larger header color box on small views
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 13:23:07 +01:00
snipe
783f0c113d Merge pull request #17009 from marcusmoore/chore/replace-user-skin-macro
Replace `skin` and `user_skin` macros with blade component
2025-05-29 13:07:47 +01:00
snipe
75a839cc21 Merge pull request #17010 from marcusmoore/chore/replace-two-factor-select-macro
Replace `two_factor_options` macro
2025-05-29 13:07:23 +01:00
snipe
577b5586b4 Merge pull request #17025 from akemidx/clear_button_on_date_picker
FIXED: Clear Button actually Clearing Dates on Date Picker
2025-05-29 12:37:56 +01:00
snipe
1474a16148 Merge pull request #17024 from akemidx/created_at_date_picker
ADDED: Created At date picker on Custom Reports
2025-05-29 12:37:17 +01:00
snipe
9baa2000e1 Merge pull request #17026 from marcusmoore/bug/translate-email-format
Reference correct translation string
2025-05-29 12:36:08 +01:00
snipe
d0624dbefe Merge pull request #17027 from akemidx/bug/sc-29295
FIXED: Translation strings in Username/Email formats
2025-05-29 12:34:47 +01:00
snipe
ecb6e8d9a9 Fixed route for custom fields index
Signed-off-by: snipe <snipe@snipe.net>
2025-05-29 12:24:22 +01:00
Robert-Azelis
bbb299faf2 Update AssetModelsController.php 2025-05-29 08:42:29 +02:00
snipe
ba3b55cab0 Merge pull request #17022 from marcusmoore/bug/sc-29281
Avoid dividing by zero in DefaultLabel
2025-05-29 02:42:41 +01:00
akemidx
67acca7bc8 fixing two translation strings. 2025-05-28 20:21:05 -04:00
Marcus Moore
e4b33c3b56 Reference correct translation string 2025-05-28 16:36:39 -07:00
akemidx
ed43c73cec clearing the date pickers 2025-05-28 18:48:22 -04:00
akemidx
a6fa795b41 clearing the date pickers 2025-05-28 18:48:08 -04:00
akemidx
3a6bac2e63 date picker 2025-05-28 18:43:24 -04:00
akemidx
8e01c05e42 date picker 2025-05-28 18:43:14 -04:00
akemidx
fabf9281e9 date picker 2025-05-28 18:35:43 -04:00
Marcus Moore
6588d409b8 Add validation 2025-05-28 14:45:03 -07:00
snipe
92a3421a4e Merge pull request #17020 from akemidx/column_persist_on_assigned_assets
FIXED: Column persist on User's Self View of Assigned Assets
2025-05-28 22:44:36 +01:00
Marcus Moore
a42147ff34 Enforce min of .1 for label width and height 2025-05-28 14:05:29 -07:00
Marcus Moore
0df1bc6894 Scaffold test 2025-05-28 14:04:12 -07:00
akemidx
9317076c5e adding cookie for Assigned Assets 2025-05-28 16:39:37 -04:00
snipe
1b5525c51f Added status label to view blade, variable to controller method
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 20:51:21 +01:00
snipe
6019c80c7b Added blade element
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 20:50:59 +01:00
Marcus Moore
aaa6cb24d4 Scaffold test 2025-05-28 11:28:23 -07:00
snipe
1ef5ad500a Merge pull request #17017 from grokability/localization/translations-2025-05-28
Updated translations
2025-05-28 18:34:15 +01:00
snipe
9468acedfa Updated languages
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 18:20:02 +01:00
snipe
6a951b6357 Merge pull request #17012 from Robert-Azelis/patch-8
API Locations - added company_id for API request
2025-05-28 15:23:08 +01:00
snipe
95f7742259 Removed extra a href
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 15:21:23 +01:00
snipe
9f795306e5 Merge pull request #17014 from grokability/fix_breadcrumb_crash
Manually add API headers
2025-05-28 15:19:05 +01:00
snipe
6feaff1e7b Removed blade::render
Signed-off-by: snipe <snipe@snipe.net>
2025-05-28 15:12:14 +01:00
Robert-Azelis
9a168354ae Update AssetModelsController.php - added requestable for API request 2025-05-28 15:50:19 +02:00
Robert-Azelis
309d242c4d Update LocationsController.php - added company_id for API request 2025-05-28 15:45:22 +02:00
snipe
5c174f829e Merge pull request #16986 from grokability/api_throttle_headers
Fixed  #16961 - Manually add API headers
2025-05-28 13:47:16 +01:00
Marcus Moore
3c428f2d7b Inline two_factor_options macro 2025-05-27 16:40:08 -07:00
Marcus Moore
6833716576 Remove skin macro 2025-05-27 16:22:43 -07:00
Marcus Moore
bd374d031a Improve component name 2025-05-27 16:22:28 -07:00
Marcus Moore
ef26f48f60 Adapt for regular "skin" macro 2025-05-27 16:21:34 -07:00
Marcus Moore
b07f8525db Fix swapped yellow/yellow-dark 2025-05-27 16:13:05 -07:00
Marcus Moore
9f062701fa Add semi-colon 2025-05-27 16:03:22 -07:00
Marcus Moore
2c452daddf Replace user_skin macro with blade component 2025-05-27 16:00:47 -07:00
snipe
53a82d3f4d Merge pull request #17007 from marcusmoore/bug/sc-29278
Ensure boolean returned from method with boolean return type
2025-05-27 22:00:04 +02:00
Marcus Moore
b5b8816279 Avoid returning null from method that should return a boolean 2025-05-27 11:37:48 -07:00
snipe
7bc4127e8c Removed dupe header
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 15:01:54 +01:00
snipe
06158cc413 Add timestamp header
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 14:58:57 +01:00
snipe
cb49e7c9a6 Updated comments
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 14:32:47 +01:00
snipe
1822027a8f Extend the built-in ThrottleRequests middleware from Laravel
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 14:04:24 +01:00
snipe
c8dabc25e3 Added comment
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 14:03:56 +01:00
snipe
f2b10eeee8 Re-do the initial change :(
Signed-off-by: snipe <snipe@snipe.net>
2025-05-27 13:00:32 +01:00
snipe
4b52e1471c Remove unused use statement after refactor
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 18:45:21 +01:00
snipe
f6bba03375 Fixed dupe semicolon
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 13:50:19 +01:00
snipe
b3813a7121 Refactorered limiting headers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 13:48:50 +01:00
snipe
eb2a1396ca Merge pull request #16999 from grokability/api_audt_fix
Better messaging when an asset fails validation on quick scan
2025-05-26 14:17:14 +02:00
snipe
0fae18c4ba Better handle missing asset payload because of RMB
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 13:14:28 +01:00
Andras Kovacs
25ac83e944 Bumped container image versions 2025-05-26 13:07:51 +02:00
snipe
a82e65e190 Add @Tinyblargon as a contributor 2025-05-26 11:09:12 +01:00
snipe
187bb90de0 Merge pull request #16993 from Tinyblargon/fix-16992
fix: `PHP_UPLOAD_LIMIT` not set for PHP 8.3
2025-05-26 12:08:36 +02:00
snipe
293648582a Improvements to API headers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-26 10:52:14 +01:00
Tinyblargon
51a306993c fix: PHP_UPLOAD_LIMIT not set for PHP 8.3 2025-05-25 16:23:17 +02:00
snipe
ec1851fa84 More small carbon fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 19:27:58 +01:00
snipe
bbe748dbd3 Removed noisy log
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 19:05:39 +01:00
snipe
406e8c5874 Added test
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 18:25:36 +01:00
snipe
a4f71a9f0a Manually add API headers
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 18:25:29 +01:00
snipe
3748498523 Merge pull request #16985 from grokability/move_faker_take_2
Moved faker out of dev reqs for seeding
2025-05-23 18:34:45 +02:00
snipe
49d11103f7 Fixed test namespace
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 17:28:40 +01:00
snipe
dce9060820 Moved faker out of dev reqs for seeding
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 17:28:00 +01:00
snipe
de6206ce78 Merge pull request #16982 from grokability/fixes#16958-expiring-assets-unarchived
Fixed #16958 - exclude archived assets from expiring assets report
2025-05-23 15:51:27 +02:00
snipe
a181ba308a Fixed #16958 - exclude archived assets from expiring assets report
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 14:48:54 +01:00
snipe
5a9ac01cf8 Set audit warnings as int
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 14:30:56 +01:00
snipe
a5cd306b1d Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 13:12:30 +01:00
snipe
57d3abab5f Merge pull request #16979 from marcusmoore/bug/sc-29178
Improved creator on accessory show page
2025-05-23 14:05:28 +02:00
snipe
b6eb3185d5 Merge pull request #16968 from marcusmoore/bug/sc-29233
Fixed potential slack webhook setting inconsistencies
2025-05-23 14:05:01 +02:00
snipe
286f78778c Merge pull request #16980 from uberbrady/add_new_checkin_checkout_counters_tests_rebased
New tests for checkin/checkout counters
2025-05-23 13:55:38 +02:00
Brady Wetherington
4b95790e2f WIP: new tests for checkin/checkout counters
note that the test isnt going to work

More WIP for checkout counters

Got all tests passing
2025-05-23 12:37:29 +01:00
snipe
97883971f7 Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 10:43:32 +01:00
snipe
b3c12d4ee6 Wider text fields
Signed-off-by: snipe <snipe@snipe.net>
2025-05-23 10:42:24 +01:00
Marcus Moore
bea426d8e6 Add test 2025-05-22 16:54:52 -07:00
Marcus Moore
be60370eae Add test 2025-05-22 16:33:45 -07:00
Marcus Moore
7cc216eb38 Extract variable 2025-05-22 15:05:24 -07:00
Marcus Moore
8c90efe745 Add authorization check 2025-05-22 15:02:14 -07:00
snipe
4b9f4423f6 Merge pull request #16932 from marcusmoore/fixes/webhook-checkin-checkout-fix
Improve notifications
2025-05-22 10:53:43 +01:00
Marcus Moore
932b589bd9 Do not show section if user doesn't exist 2025-05-21 17:28:41 -07:00
Marcus Moore
ce18b91b03 Add translation 2025-05-21 16:49:15 -07:00
Marcus Moore
5f883310b5 Improve display of user on accessory show page 2025-05-21 16:48:26 -07:00
Marcus Moore
69cc46c2b8 Improve method name 2025-05-20 16:41:52 -07:00
Marcus Moore
dc6951f341 Improve method name 2025-05-20 16:28:49 -07:00
Marcus Moore
f7ed336f99 Remove typehint 2025-05-20 16:26:05 -07:00
Marcus Moore
47f287c031 Fix assertion 2025-05-20 16:25:22 -07:00
Marcus Moore
5739393f8d Check to make sure settings exist before attempting to update them 2025-05-20 12:45:23 -07:00
Marcus Moore
13956254ce Add migration to fix webhook settings 2025-05-20 12:27:19 -07:00
Marcus Moore
d6b69c8cc2 Fix webhook selected value in mount method 2025-05-20 12:00:58 -07:00
Marcus Moore
7a79fd18f0 Bind the selected webhook and avoid clearing from database 2025-05-20 11:40:44 -07:00
snipe
e3ffe79c4c Merge pull request #16962 from Godmartinz/audit_notifications_fix
Added dynamic properties to audit notifications
2025-05-20 12:34:33 +02:00
Marcus Moore
043325b966 Merge branch 'develop' into fixes/webhook-checkin-checkout-fix
# Conflicts:
#	app/Listeners/CheckoutableListener.php
2025-05-19 14:41:05 -07:00
Marcus Moore
a2696b799f Standardize test names 2025-05-19 14:33:18 -07:00
snipe
eb8ef37808 Merge pull request #16963 from Godmartinz/fix-double-scroll-bar-permission-groups
Removes double scrollbar from groups
2025-05-19 20:32:30 +02:00
Godfrey M
8416c6df05 removes double scrollbar 2025-05-19 11:21:52 -07:00
Godfrey M
3aa4814342 allow dynamic assignment to audit notifications 2025-05-19 11:08:03 -07:00
snipe
186f322bb5 Merge pull request #16953 from uberbrady/fix_checkin_emails
Make checkin emails not send when not configured to be
2025-05-16 11:14:31 +02:00
Brady Wetherington
28e2e7c924 Get rid of more editorialization 2025-05-16 10:57:34 +02:00
Brady Wetherington
97351028b5 Get rid of editorializing in comments 2025-05-16 10:56:36 +02:00
Brady Wetherington
0ef0863b59 Make checkin emails not send unless the send-emails attribute is set on the category 2025-05-16 10:48:17 +02:00
Marcus Moore
2e80b4ace8 WIP 2025-05-15 17:50:07 -07:00
Marcus Moore
0d896c2ef6 Extract method 2025-05-15 17:44:27 -07:00
Marcus Moore
dc9df04237 WIP 2025-05-15 17:40:51 -07:00
Marcus Moore
b469a64db3 WIP 2025-05-15 17:35:28 -07:00
Marcus Moore
73f19ff4e7 Scaffold tests 2025-05-15 17:30:06 -07:00
Marcus Moore
22b4fac3ee Formatting 2025-05-15 17:24:56 -07:00
Marcus Moore
c97884c8b0 WIP 2025-05-15 16:59:10 -07:00
Marcus Moore
b972fb514a Remove type hint 2025-05-15 16:54:46 -07:00
Marcus Moore
a3871bd1f2 WIP 2025-05-15 16:47:29 -07:00
Marcus Moore
2069f99b2b WIP 2025-05-15 16:42:20 -07:00
Marcus Moore
882d55fd09 Improve tests 2025-05-15 16:31:07 -07:00
Marcus Moore
68ef975726 WIP 2025-05-15 16:22:31 -07:00
Marcus Moore
0685ff3818 WIP 2025-05-15 16:12:51 -07:00
Marcus Moore
b28839d907 Scaffold tests 2025-05-15 15:57:52 -07:00
Marcus Moore
7f0a947de4 WIP 2025-05-15 15:43:57 -07:00
Marcus Moore
c8fc4afe65 Fix method name 2025-05-15 15:31:18 -07:00
Marcus Moore
3b7162cb02 Improve test setup 2025-05-15 15:30:16 -07:00
Marcus Moore
4245456382 Scaffold more tests 2025-05-15 15:27:15 -07:00
Marcus Moore
cdf43e31e2 Remove randomness from factory 2025-05-15 15:14:01 -07:00
Marcus Moore
f19d6b3c52 Scaffold tests 2025-05-15 15:13:50 -07:00
snipe
ca66e29072 Merge pull request #16946 from marcusmoore/bug/sc-29166
Gracefully handle error when editing of soft deleted users
2025-05-15 22:46:48 +02:00
Marcus Moore
6ff76a12f8 Strengthen test scenario 2025-05-15 13:39:27 -07:00
Marcus Moore
8b13997597 Merge branch 'develop' into fixes/webhook-checkin-checkout-fix
# Conflicts:
#	app/Listeners/CheckoutableListener.php
2025-05-15 13:38:20 -07:00
Marcus Moore
9600adee6b Don't allow viewing edit page if user soft deleted 2025-05-15 11:14:15 -07:00
snipe
6eb3819492 Apply correct unique custom field validation for audit API
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 18:31:49 +02:00
snipe
95226f87bc Use same logic for auditStore for validating unique fields
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 18:02:23 +02:00
snipe
ee3ae803b9 Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:51:28 +02:00
snipe
057667c425 Removed duplicate unique display in table
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:10:47 +02:00
Marcus Moore
02fa7daa1d Require assigned_x to be integer on asset model 2025-05-14 14:13:07 -07:00
Marcus Moore
2a2acf6d1c Add failing test 2025-05-14 14:09:48 -07:00
Marcus Moore
f644642fac Add comment 2025-05-14 12:48:32 -07:00
Marcus Moore
3374a9e5a9 Add assertion 2025-05-14 12:48:11 -07:00
Marcus Moore
d3a74a5740 Use route bound user instead of re-querying 2025-05-14 12:46:34 -07:00
Marcus Moore
c458cc904a Add failing test 2025-05-14 12:45:55 -07:00
snipe
5c7b74e17e Merge pull request #16943 from grokability/small-tweaks-to-status-label-text
Clearer text on status label types
2025-05-14 17:35:40 +02:00
snipe
e07b0f65a1 Clearer text on status label types
Signed-off-by: snipe <snipe@snipe.net>
2025-05-14 17:33:12 +02:00
snipe
b06fd5bbca Merge pull request #16942 from uberbrady/quick_temp_fix_notifications
A quick check to make sure that webhooks still fire when email is off
2025-05-14 16:21:37 +02:00
Brady Wetherington
6306f78fe0 A quick check to make sure that webhooks still fire when email is off 2025-05-14 16:14:03 +02:00
snipe
da4bce0c89 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-05-14 15:02:43 +02:00
snipe
053e815206 Add @JassonCordones as a contributor 2025-05-14 15:02:43 +02:00
snipe
ab1053ecda Merge pull request #16916 from marcusmoore/bug/sc-29153
Handle potential hard exception in Asset@getImageUrl method
2025-05-14 14:52:33 +02:00
snipe
9a61a3391b Merge pull request #16921 from marcusmoore/chore/locale-select
Replace locales macro
2025-05-14 14:52:02 +02:00
snipe
d3c19e28ec Merge pull request #16918 from marcusmoore/bug/sc-29151
Handle displaying deleted creator of an accessory
2025-05-14 13:36:08 +02:00
snipe
87115f2e50 Merge branch 'develop' into bug/sc-29151 2025-05-14 13:35:38 +02:00
Marcus Moore
5f7aadfba0 Ensure CC emails are always sent for assets 2025-05-13 17:56:35 -07:00
Marcus Moore
e954e066b4 Scaffold tests 2025-05-13 17:41:19 -07:00
Marcus Moore
e336182d79 Formatting 2025-05-13 14:45:09 -07:00
Marcus Moore
3eb0743446 Separate checking for sending email and webhook notifications 2025-05-13 14:43:16 -07:00
Marcus Moore
95c1c37ab1 Invert method 2025-05-13 14:25:03 -07:00
Marcus Moore
a7054f0b1e Improve method name 2025-05-13 14:20:42 -07:00
Marcus Moore
e4bfabfabe Add failing tests 2025-05-13 14:12:51 -07:00
snipe
6b56929a06 Null operator for missing created_by record
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:44:25 +02:00
Marcus Moore
e3642bb513 Remove unneeded div 2025-05-12 17:07:17 -07:00
Marcus Moore
ca7c416e19 Remove the replaced locales form macro 2025-05-12 16:35:40 -07:00
snipe
ac6d964e28 Merge pull request #16920 from marcusmoore/fixes/remove-outline-from-label
Remove logo outline from L7162_B
2025-05-12 23:04:49 +01:00
Marcus Moore
e2772c816d Remove label logo outline from L7162_B label 2025-05-12 13:58:13 -07:00
Marcus Moore
916f7401f3 Handle creator being deleted 2025-05-12 12:30:32 -07:00
Marcus Moore
b53a71d523 Add failing test 2025-05-12 12:30:06 -07:00
Marcus Moore
510a2b0889 Formatting 2025-05-12 12:03:15 -07:00
Marcus Moore
73057454c6 Fix bug in getImageUrl method 2025-05-12 11:56:40 -07:00
Marcus Moore
5a6cf2a296 Add test for existing functionality 2025-05-12 11:44:25 -07:00
snipe
9e3e04521e Merge pull request #16900 from marcusmoore/fixes/user-full-name-accessor
Handle settings not being available in full name accessor
2025-05-10 12:26:19 +01:00
snipe
65dfbd02fe Use develop branch
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 21:00:55 +01:00
snipe
649ab53320 Updated codacy link
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 21:00:18 +01:00
snipe
9250624f79 Merge pull request #16909 from grokability/fixes-#16554-category-delete
Fixed #16554 - Added models to deletable check
2025-05-09 19:23:35 +01:00
snipe
995e2090f5 Added/updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 19:17:53 +01:00
snipe
9b91584776 Added models to deletable check
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 19:17:48 +01:00
snipe
8fd97ea501 Merge pull request #16908 from grokability/bug/sc-28724
Fixed #16535 - more info to side rail in accessories
2025-05-09 18:03:05 +01:00
snipe
a80b9ab362 Fixed #16535 - more info to side rail in accessories
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:48:02 +01:00
snipe
556e1081b3 Added two more selectors for byod
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:23:08 +01:00
snipe
b070916f0b Merge pull request #16907 from grokability/add_ids_to_menus
Fixed #16456 - added ids to sidenav options and bod
2025-05-09 17:22:19 +01:00
snipe
940caf14b0 Added ids to menu items
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:09:59 +01:00
snipe
76da1d6663 Added class to checkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 17:04:19 +01:00
snipe
bafff9020a Merge pull request #16611 from Godmartinz/MS_teams_deprecation_update
Reworked MS Teams deprecation warnings and notifications visibility
2025-05-09 16:38:25 +01:00
snipe
0d5dca6456 Fixed #16690 - fallback to category image if no model image is present
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 14:46:27 +01:00
snipe
0f9b7119c0 Merge pull request #16905 from grokability/fixes_#16901
Fixed #16901 - use default currency for asset maintenance cost
2025-05-09 12:45:46 +01:00
snipe
d4181549e8 Fixes #16901 - use default currency for maintenance cost display
Signed-off-by: snipe <snipe@snipe.net>
2025-05-09 12:43:49 +01:00
Marcus Moore
d57f56e44f Handle settings not being available 2025-05-08 16:20:26 -07:00
Marcus Moore
f7777ca8a5 Replace Form::locales on user setup 2025-05-08 15:38:49 -07:00
Marcus Moore
fce5530bc7 Replace Form::locales on bulk edit users page 2025-05-08 15:31:41 -07:00
Marcus Moore
ad1530e9ff Replace Form::locales on user edit page 2025-05-08 15:17:15 -07:00
Marcus Moore
abb2dcbbe4 Replace Form::locales on localization page 2025-05-08 15:05:48 -07:00
Marcus Moore
437499c5df Fix input name 2025-05-08 15:04:45 -07:00
Marcus Moore
f739c2c84a Introduce locale select component and make replacement on profile page 2025-05-08 15:02:35 -07:00
Chris Olin
248a05a916 adds support for continuous 53mm and 0.59in printers 2025-05-06 11:26:52 -04:00
Chris Olin
2c141579dd adds support for GenericTape label printers, includes class for 53mm tape printer 2025-05-06 11:26:40 -04:00
Godfrey M
e3a2397b74 removed hiding the notifications icon 2025-05-05 10:28:08 -07:00
Godfrey M
3b34654dd7 Merge branch 'develop' into MS_teams_deprecation_update
# Conflicts:
#	resources/lang/en-US/admin/settings/message.php
2025-05-05 09:33:32 -07:00
Godfrey M
4b6437854c swapped out hard coded text with translation 2025-05-05 09:31:23 -07:00
Godfrey M
c385b4a082 remove testing lines 2025-04-09 12:11:46 -07:00
Godfrey M
1b961346f0 added withInput to the redirects 2025-04-09 12:11:31 -07:00
Godfrey M
100db23210 add checks that the target is not null, and redirects back with error messages 2025-04-09 11:54:59 -07:00
Godfrey M
3e980a4c57 set location if target is set 2025-04-09 11:11:59 -07:00
Godfrey M
9a3ac41370 add default location as a fallback to asset validation 2025-04-09 11:02:45 -07:00
Godfrey M
4ef161214d notification icon only appears when there are notifications 2025-04-01 12:01:35 -07:00
Godfrey M
29d0380db3 reword warning messages, remove warning if webhook cleared and saved, deprecations only for superadmins 2025-04-01 11:53:32 -07:00
snipe
0b48fd1465 Removed extra headers
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 13:05:31 +00:00
snipe
220537fbfb Updated presenter name
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 12:59:57 +00:00
snipe
df5437647b Add optional serial value in presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 12:43:38 +00:00
snipe
92b2da9b1b Added history tab to components
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 11:48:38 +00:00
snipe
ef56177372 Use presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 11:48:31 +00:00
snipe
cb7822576f Use new presenters
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 11:48:19 +00:00
snipe
7ba361b10d Use date formatter for filestable
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 10:57:54 +00:00
snipe
55694fa2fc Added strings
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 10:57:40 +00:00
snipe
c825878c46 Added history presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-03-10 10:57:33 +00:00
snipe
80a69bfe90 Revert datetime to date
Signed-off-by: snipe <snipe@snipe.net>
2025-03-06 18:09:27 +00:00
snipe
d4dc8d2b79 Remove action_date from loggable as a changed field
Signed-off-by: snipe <snipe@snipe.net>
2025-03-06 17:43:07 +00:00
snipe
4e3df93349 Change action_date display to date from datetime
Signed-off-by: snipe <snipe@snipe.net>
2025-03-06 16:16:17 +00:00
snipe
38efc62900 Add index on action_date, copy from created_at
Signed-off-by: snipe <snipe@snipe.net>
2025-03-06 16:01:46 +00:00
Timo Schwarzer
a2ff8f9609 Add Department Manager to single and multiple user views 2024-07-15 12:27:29 +02:00
1506 changed files with 30825 additions and 7792 deletions

View File

@@ -4153,6 +4153,42 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "JassonCordones",
"name": "Jasson",
"avatar_url": "https://avatars.githubusercontent.com/u/4875039?v=4",
"profile": "http://jassoncordones.github.io",
"contributions": [
"code"
]
},
{
"login": "Tinyblargon",
"name": "Okean",
"avatar_url": "https://avatars.githubusercontent.com/u/76069640?v=4",
"profile": "https://github.com/Tinyblargon",
"contributions": [
"code"
]
},
{
"login": "amedranogil",
"name": "Alejandro Medrano",
"avatar_url": "https://avatars.githubusercontent.com/u/6515064?v=4",
"profile": "https://www.lst.tfo.upm.es/alejandro-medrano/",
"contributions": [
"code"
]
},
{
"login": "lukaskraic",
"name": "Lukas Kraic",
"avatar_url": "https://avatars.githubusercontent.com/u/58696401?v=4",
"profile": "https://github.com/lukaskraic",
"contributions": [
"code"
]
} }
] ]
} }

View File

@@ -10,10 +10,10 @@ name: Codacy Security Scan
on: on:
push: push:
branches: [ master ] branches: [ develop ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ master ] branches: [ develop ]
schedule: schedule:
- cron: '36 23 * * 3' - cron: '36 23 * * 3'

View File

@@ -1,606 +1,73 @@
Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far: Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start --> | [<img src="https://avatars3.githubusercontent.com/u/197404?v=3" width="110px;"/><br /><sub>snipe</sub>](http://www.snipe.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snipe "Code") [🚇](#infra-snipe "Infrastructure (Hosting, Build-Tools, etc)") [📖](https://github.com/snipe/snipe-it/commits?author=snipe "Documentation") [⚠️](https://github.com/snipe/snipe-it/commits?author=snipe "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asnipe "Bug reports") [🎨](#design-snipe "Design") [👀](#review-snipe "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/36335?v=3" width="110px;"/><br /><sub>Brady Wetherington</sub>](http://www.uberbrady.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=uberbrady "Code") [📖](https://github.com/snipe/snipe-it/commits?author=uberbrady "Documentation") [🚇](#infra-uberbrady "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-uberbrady "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/3803132?v=3" width="110px;"/><br /><sub>Daniel Meltzer</sub>](https://github.com/dmeltzer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Tests") [📖](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1609106?v=3" width="110px;"/><br /><sub>Michael T</sub>](http://www.tuckertechonline.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mtucker6784 "Code") | [<img src="https://avatars2.githubusercontent.com/u/3274937?v=3" width="110px;"/><br /><sub>madd15</sub>](https://github.com/madd15)<br />[📖](https://github.com/snipe/snipe-it/commits?author=madd15 "Documentation") [💬](#question-madd15 "Answering Questions") | [<img src="https://avatars2.githubusercontent.com/u/894126?v=3" width="110px;"/><br /><sub>Vincent Sposato</sub>](https://github.com/vsposato)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vsposato "Code") | [<img src="https://avatars0.githubusercontent.com/u/1639757?v=3" width="110px;"/><br /><sub>Andrea Bergamasco</sub>](https://github.com/vjandrea)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vjandrea "Code") |
<!-- markdownlint-disable --> | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
<table> | [<img src="https://avatars0.githubusercontent.com/u/10640152?v=3" width="110px;"/><br /><sub>Karol</sub>](https://github.com/kpawelski)<br />[🌍](#translation-kpawelski "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=kpawelski "Code") | [<img src="https://avatars3.githubusercontent.com/u/600106?v=3" width="110px;"/><br /><sub>morph027</sub>](http://blog.morph027.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=morph027 "Code") | [<img src="https://avatars3.githubusercontent.com/u/22935755?v=3" width="110px;"/><br /><sub>fvleminckx</sub>](https://github.com/fvleminckx)<br />[🚇](#infra-fvleminckx "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars2.githubusercontent.com/u/15633547?v=3" width="110px;"/><br /><sub>itsupportcmsukorg</sub>](https://github.com/itsupportcmsukorg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg "Code") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg "Bug reports") | [<img src="https://avatars3.githubusercontent.com/u/12373799?v=3" width="110px;"/><br /><sub>Frank</sub>](https://override.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=base-zero "Code") | [<img src="https://avatars0.githubusercontent.com/u/10137?v=3" width="110px;"/><br /><sub>Deleted user</sub>](https://github.com/ghost)<br />[🌍](#translation-ghost "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=ghost "Code") | [<img src="https://avatars1.githubusercontent.com/u/10802313?v=3" width="110px;"/><br /><sub>tiagom62</sub>](https://github.com/tiagom62)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tiagom62 "Code") [🚇](#infra-tiagom62 "Infrastructure (Hosting, Build-Tools, etc)") |
<tbody> | [<img src="https://avatars3.githubusercontent.com/u/2389047?v=3" width="110px;"/><br /><sub>Ryan Stafford</sub>](https://github.com/rystaf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rystaf "Code") | [<img src="https://avatars2.githubusercontent.com/u/10345935?v=3" width="110px;"/><br /><sub>Eammon Hanlon</sub>](https://github.com/ehanlon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ehanlon "Code") | [<img src="https://avatars0.githubusercontent.com/u/441924?v=3" width="110px;"/><br /><sub>zjean</sub>](https://github.com/zjean)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zjean "Code") | [<img src="https://avatars0.githubusercontent.com/u/12660103?v=3" width="110px;"/><br /><sub>Matthias Frei</sub>](http://www.frei.media)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FREImedia "Code") | [<img src="https://avatars0.githubusercontent.com/u/3767518?v=3" width="110px;"/><br /><sub>opsydev</sub>](https://github.com/opsydev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=opsydev "Code") | [<img src="https://avatars1.githubusercontent.com/u/82290?v=3" width="110px;"/><br /><sub>Daniel Dreier</sub>](http://www.ddreier.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ddreier "Code") | [<img src="https://avatars0.githubusercontent.com/u/23448?v=3" width="110px;"/><br /><sub>Nikolai Prokoschenko</sub>](http://rassie.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rassie "Code") |
<tr> | [<img src="https://avatars0.githubusercontent.com/u/13452757?v=3" width="110px;"/><br /><sub>Drew</sub>](https://github.com/YetAnotherCodeMonkey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey "Code") | [<img src="https://avatars0.githubusercontent.com/u/1342320?v=3" width="110px;"/><br /><sub>Walter</sub>](https://github.com/merid14)<br />[💻](https://github.com/snipe/snipe-it/commits?author=merid14 "Code") | [<img src="https://avatars3.githubusercontent.com/u/11254614?v=3" width="110px;"/><br /><sub>Petr Baloun</sub>](https://github.com/balous)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balous "Code") | [<img src="https://avatars0.githubusercontent.com/u/6117660?v=3" width="110px;"/><br /><sub>reidblomquist</sub>](https://github.com/reidblomquist)<br />[📖](https://github.com/snipe/snipe-it/commits?author=reidblomquist "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/539914?v=3" width="110px;"/><br /><sub>Mathieu Kooiman</sub>](https://github.com/mathieuk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mathieuk "Code") | [<img src="https://avatars3.githubusercontent.com/u/6606421?v=3" width="110px;"/><br /><sub>csayre</sub>](https://github.com/csayre)<br />[📖](https://github.com/snipe/snipe-it/commits?author=csayre "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/768488?v=3" width="110px;"/><br /><sub>Adam Dunson</sub>](https://github.com/adamdunson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamdunson "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://www.snipe.net"><img src="https://avatars3.githubusercontent.com/u/197404?v=3?s=110" width="110px;" alt="snipe"/><br /><sub><b>snipe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Code">💻</a> <a href="#infra-snipe" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Documentation">📖</a> <a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Asnipe" title="Bug reports">🐛</a> <a href="#design-snipe" title="Design">🎨</a> <a href="https://github.com/snipe/snipe-it/pulls?q=is%3Apr+reviewed-by%3Asnipe" title="Reviewed Pull Requests">👀</a></td> | [<img src="https://avatars0.githubusercontent.com/u/5547470?v=3" width="110px;"/><br /><sub>Hereward</sub>](https://github.com/thehereward)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thehereward "Code") | [<img src="https://avatars0.githubusercontent.com/u/5802977?v=3" width="110px;"/><br /><sub>swoopdk</sub>](https://github.com/swoopdk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=swoopdk "Code") | [<img src="https://avatars1.githubusercontent.com/u/3470403?v=3" width="110px;"/><br /><sub>Abdullah Alansari</sub>](https://linkedin.com/in/ahimta)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Ahimta "Code") | [<img src="https://avatars0.githubusercontent.com/u/796443?v=3" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues "Code") | [<img src="https://avatars0.githubusercontent.com/u/614564?v=3" width="110px;"/><br /><sub>Patrick Gallagher</sub>](http://macadmincorner.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=patgmac "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/7165922?v=3" width="110px;"/><br /><sub>Miliamber</sub>](https://github.com/Miliamber)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Miliamber "Code") | [<img src="https://avatars3.githubusercontent.com/u/861766?v=3" width="110px;"/><br /><sub>hawk554</sub>](https://github.com/hawk554)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hawk554 "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://www.uberbrady.com"><img src="https://avatars0.githubusercontent.com/u/36335?v=3?s=110" width="110px;" alt="Brady Wetherington"/><br /><sub><b>Brady Wetherington</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=uberbrady" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=uberbrady" title="Documentation">📖</a> <a href="#infra-uberbrady" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/snipe/snipe-it/pulls?q=is%3Apr+reviewed-by%3Auberbrady" title="Reviewed Pull Requests">👀</a></td> | [<img src="https://avatars1.githubusercontent.com/u/1695622?v=3" width="110px;"/><br /><sub>Justin Kerr</sub>](http://jbirdkerr.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jbirdkerr "Code") | [<img src="https://avatars3.githubusercontent.com/u/11426176?v=3" width="110px;"/><br /><sub>Ira W. Snyder</sub>](http://www.irasnyder.com/devel/)<br />[📖](https://github.com/snipe/snipe-it/commits?author=irasnyd "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/2475759?v=3" width="110px;"/><br /><sub>Aladin Alaily</sub>](https://github.com/aalaily)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aalaily "Code") | [<img src="https://avatars0.githubusercontent.com/u/10247644?v=3" width="110px;"/><br /><sub>Chase Hansen</sub>](https://github.com/kobie-chasehansen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen "Code") [💬](#question-kobie-chasehansen "Answering Questions") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen "Bug reports") | [<img src="https://avatars2.githubusercontent.com/u/13545400?v=3" width="110px;"/><br /><sub>IDM Helpdesk</sub>](https://github.com/IDM-Helpdesk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk "Code") | [<img src="https://avatars2.githubusercontent.com/u/614439?v=3" width="110px;"/><br /><sub>Kai</sub>](http://balticer.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balticer "Code") | [<img src="https://avatars1.githubusercontent.com/u/8762511?v=3" width="110px;"/><br /><sub>Michael Daniels</sub>](http://www.michaeldaniels.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mdaniels5757 "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dmeltzer"><img src="https://avatars0.githubusercontent.com/u/3803132?v=3?s=110" width="110px;" alt="Daniel Meltzer"/><br /><sub><b>Daniel Meltzer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Documentation">📖</a></td> | [<img src="https://avatars3.githubusercontent.com/u/1532660?v=3" width="110px;"/><br /><sub>Tom Castleman</sub>](http://tomcastleman.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tomcastleman "Code") | [<img src="https://avatars3.githubusercontent.com/u/10723243?v=3" width="110px;"/><br /><sub>Daniel Nemanic</sub>](https://github.com/DanielNemanic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielNemanic "Code") | [<img src="https://avatars0.githubusercontent.com/u/150648?v=3" width="110px;"/><br /><sub>SouthWolf</sub>](https://github.com/southwolf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=southwolf "Code") | [<img src="https://avatars2.githubusercontent.com/u/131616?v=3" width="110px;"/><br /><sub>Ivar Nesje</sub>](https://github.com/ivarne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ivarne "Code") | [<img src="https://avatars1.githubusercontent.com/u/62333?v=3" width="110px;"/><br /><sub>Jérémy Benoist</sub>](http://www.j0k3r.net)<br />[📖](https://github.com/snipe/snipe-it/commits?author=j0k3r "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/724344?v=3" width="110px;"/><br /><sub>Chris Leathley</sub>](https://github.com/cleathley)<br />[🚇](#infra-cleathley "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars0.githubusercontent.com/u/972498?v=3" width="110px;"/><br /><sub>splaer</sub>](https://github.com/splaer)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asplaer "Bug reports") [💻](https://github.com/snipe/snipe-it/commits?author=splaer "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://www.tuckertechonline.com"><img src="https://avatars0.githubusercontent.com/u/1609106?v=3?s=110" width="110px;" alt="Michael T"/><br /><sub><b>Michael T</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mtucker6784" title="Code">💻</a></td> | [<img src="https://avatars1.githubusercontent.com/u/967362?v=3" width="110px;"/><br /><sub>Joe Ferguson</sub>](http://www.joeferguson.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=svpernova09 "Code") | [<img src="https://avatars3.githubusercontent.com/u/6108682?v=3" width="110px;"/><br /><sub>diwanicki</sub>](https://github.com/diwanicki)<br />[💻](https://github.com/snipe/snipe-it/commits?author=diwanicki "Code") [📖](https://github.com/snipe/snipe-it/commits?author=diwanicki "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/2527115?v=3" width="110px;"/><br /><sub>Lee Thoong Ching</sub>](https://github.com/pakkua80)<br />[📖](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Documentation") [💻](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Code") | [<img src="https://avatars1.githubusercontent.com/u/461491?v=3" width="110px;"/><br /><sub>Marek Šuppa</sub>](http://shu.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrshu "Code") | [<img src="https://avatars1.githubusercontent.com/u/8693762?v=3" width="110px;"/><br /><sub>Juan J. Martinez</sub>](https://github.com/mizar1616)<br />[🌍](#translation-mizar1616 "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1458388?v=3" width="110px;"/><br /><sub>R Ryan Dial</sub>](https://github.com/rrdial)<br />[🌍](#translation-rrdial "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2871745?v=3" width="110px;"/><br /><sub>Andrej Manduch</sub>](https://github.com/burlito)<br />[📖](https://github.com/snipe/snipe-it/commits?author=burlito "Documentation") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madd15"><img src="https://avatars2.githubusercontent.com/u/3274937?v=3?s=110" width="110px;" alt="madd15"/><br /><sub><b>madd15</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=madd15" title="Documentation">📖</a> <a href="#question-madd15" title="Answering Questions">💬</a></td> | [<img src="https://avatars0.githubusercontent.com/u/8341172?v=3" width="110px;"/><br /><sub>Jay Richards</sub>](http://www.cordeos.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=technogenus "Code") | [<img src="https://avatars2.githubusercontent.com/u/7295127?v=3" width="110px;"/><br /><sub>Alexander Innes</sub>](https://necurity.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leostat "Code") | [<img src="https://avatars2.githubusercontent.com/u/334485?v=3" width="110px;"/><br /><sub>Danny Garcia</sub>](https://buzzedword.codes)<br />[💻](https://github.com/snipe/snipe-it/commits?author=buzzedword "Code") | [<img src="https://avatars2.githubusercontent.com/u/366855?v=3" width="110px;"/><br /><sub>archpoint</sub>](https://github.com/archpoint)<br />[💻](https://github.com/snipe/snipe-it/commits?author=archpoint "Code") | [<img src="https://avatars1.githubusercontent.com/u/67991?v=3" width="110px;"/><br /><sub>Jake McGraw</sub>](http://www.jakemcgraw.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jakemcgraw "Code") | [<img src="https://avatars1.githubusercontent.com/u/1714374?v=3" width="110px;"/><br /><sub>FleischKarussel</sub>](https://github.com/FleischKarussel)<br />[📖](https://github.com/snipe/snipe-it/commits?author=FleischKarussel "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/319644?v=3" width="110px;"/><br /><sub>Dylan Yi</sub>](https://github.com/feeva)<br />[💻](https://github.com/snipe/snipe-it/commits?author=feeva "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vsposato"><img src="https://avatars2.githubusercontent.com/u/894126?v=3?s=110" width="110px;" alt="Vincent Sposato"/><br /><sub><b>Vincent Sposato</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vsposato" title="Code">💻</a></td> | [<img src="https://avatars2.githubusercontent.com/u/857740?v=3" width="110px;"/><br /><sub>Gil Rutkowski</sub>](http://FlashingCursor.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=flashingcursor "Code") | [<img src="https://avatars3.githubusercontent.com/u/129360?v=3" width="110px;"/><br /><sub>Desmond Morris</sub>](http://www.desmondmorris.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=desmondmorris "Code") | [<img src="https://avatars2.githubusercontent.com/u/52936?v=3" width="110px;"/><br /><sub>Nick Peelman</sub>](http://peelman.us)<br />[💻](https://github.com/snipe/snipe-it/commits?author=peelman "Code") | [<img src="https://avatars0.githubusercontent.com/u/53161?v=3" width="110px;"/><br /><sub>Abraham Vegh</sub>](https://abrahamvegh.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=abrahamvegh "Code") | [<img src="https://avatars0.githubusercontent.com/u/2818680?v=3" width="110px;"/><br /><sub>Mohamed Rashid</sub>](https://github.com/rashivkp)<br />[📖](https://github.com/snipe/snipe-it/commits?author=rashivkp "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/1509456?v=3" width="110px;"/><br /><sub>Kasey</sub>](http://hinchk.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=HinchK "Code") | [<img src="https://avatars2.githubusercontent.com/u/10522541?v=3" width="110px;"/><br /><sub>Brett</sub>](https://github.com/BrettFagerlund)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=BrettFagerlund "Tests") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vjandrea"><img src="https://avatars0.githubusercontent.com/u/1639757?v=3?s=110" width="110px;" alt="Andrea Bergamasco"/><br /><sub><b>Andrea Bergamasco</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vjandrea" title="Code">💻</a></td> | [<img src="https://avatars2.githubusercontent.com/u/16108587?v=3" width="110px;"/><br /><sub>Jason Spriggs</sub>](http://jasonspriggs.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonspriggs "Code") | [<img src="https://avatars2.githubusercontent.com/u/1134568?v=3" width="110px;"/><br /><sub>Nate Felton</sub>](http://n8felton.wordpress.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=n8felton "Code") | [<img src="https://avatars2.githubusercontent.com/u/14036694?v=3" width="110px;"/><br /><sub>Manasses Ferreira</sub>](http://homepages.dcc.ufmg.br/~manassesferreira)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manassesferreira "Code") | [<img src="https://avatars0.githubusercontent.com/u/15913949?v=3" width="110px;"/><br /><sub>Steve</sub>](https://github.com/steveelwood)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=steveelwood "Tests") | [<img src="https://avatars1.githubusercontent.com/u/3361683?v=3" width="110px;"/><br /><sub>matc</sub>](http://twitter.com/matc)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=matc "Tests") | [<img src="https://avatars3.githubusercontent.com/u/7405702?v=3" width="110px;"/><br /><sub>Cole R. Davis</sub>](http://www.davisracingteam.com)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD "Tests") | [<img src="https://avatars2.githubusercontent.com/u/10167681?v=3" width="110px;"/><br /><sub>gibsonjoshua55</sub>](https://github.com/gibsonjoshua55)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55 "Code") |
</tr> | [<img src="https://avatars2.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://github.com/zwerch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zwerch "Code") | [<img src="https://avatars0.githubusercontent.com/u/6961695?v=4" width="110px;"/><br /><sub>Iman</sub>](https://github.com/imanghafoori1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=imanghafoori1 "Code") | [<img src="https://avatars1.githubusercontent.com/u/6551003?v=4" width="110px;"/><br /><sub>Richard Hofman</sub>](https://github.com/richardhofman6)<br />[💻](https://github.com/snipe/snipe-it/commits?author=richardhofman6 "Code") | [<img src="https://avatars0.githubusercontent.com/u/3697569?v=4" width="110px;"/><br /><sub>gizzmojr</sub>](https://github.com/gizzmojr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gizzmojr "Code") | [<img src="https://avatars3.githubusercontent.com/u/404729?v=4" width="110px;"/><br /><sub>Jenny Li</sub>](https://github.com/imjennyli)<br />[📖](https://github.com/snipe/snipe-it/commits?author=imjennyli "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/869227?v=4" width="110px;"/><br /><sub>Geoff Young</sub>](https://github.com/GeoffYoung)<br />[💻](https://github.com/snipe/snipe-it/commits?author=GeoffYoung "Code") | [<img src="https://avatars3.githubusercontent.com/u/1068477?v=4" width="110px;"/><br /><sub>Elliot Blackburn</sub>](http://www.elliotblackburn.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=BlueHatbRit "Documentation") |
<tr> | [<img src="https://avatars1.githubusercontent.com/u/6357451?v=4" width="110px;"/><br /><sub>Tõnis Ormisson</sub>](http://andmemasin.eu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TonisOrmisson "Code") | [<img src="https://avatars0.githubusercontent.com/u/449411?v=4" width="110px;"/><br /><sub>Nicolai Essig</sub>](http://www.nicolai-essig.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thakilla "Code") | [<img src="https://avatars1.githubusercontent.com/u/14809698?v=4" width="110px;"/><br /><sub>Danielle</sub>](https://github.com/techincolor)<br />[📖](https://github.com/snipe/snipe-it/commits?author=techincolor "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/18545156?v=4" width="110px;"/><br /><sub>Lawrence</sub>](https://github.com/TheVakman)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=TheVakman "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>uknzaeinozpas</sub>](https://github.com/uknzaeinozpas)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Tests") [💻](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Code") | [<img src="https://avatars3.githubusercontent.com/u/422752?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/Gelob)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Gelob "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/10672546?v=4" width="110px;"/><br /><sub>vcordes79</sub>](https://github.com/vcordes79)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vcordes79 "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kpawelski"><img src="https://avatars0.githubusercontent.com/u/10640152?v=3?s=110" width="110px;" alt="Karol"/><br /><sub><b>Karol</b></sub></a><br /><a href="#translation-kpawelski" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=kpawelski" title="Code">💻</a></td> | [<img src="https://avatars3.githubusercontent.com/u/27958330?v=4" width="110px;"/><br /><sub>fordster78</sub>](https://github.com/fordster78)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fordster78 "Code") | [<img src="https://avatars0.githubusercontent.com/u/34064225?v=4" width="110px;"/><br /><sub>CronKz</sub>](https://github.com/CronKz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CronKz "Code") [🌍](#translation-CronKz "Translation") | [<img src="https://avatars1.githubusercontent.com/u/585486?v=4" width="110px;"/><br /><sub>Tim Bishop</sub>](https://github.com/tdb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tdb "Code") | [<img src="https://avatars2.githubusercontent.com/u/5384694?v=4" width="110px;"/><br /><sub>Sean McIlvenna</sub>](https://www.seanmcilvenna.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanmcilvenna "Code") | [<img src="https://avatars3.githubusercontent.com/u/36515590?v=4" width="110px;"/><br /><sub>cepacs</sub>](https://github.com/cepacs)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/snipe/snipe-it/commits?author=cepacs "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/37537300?v=4" width="110px;"/><br /><sub>lea-mink</sub>](https://github.com/lea-mink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lea-mink "Code") | [<img src="https://avatars0.githubusercontent.com/u/7140719?v=4" width="110px;"/><br /><sub>Hannah Tinkler</sub>](https://github.com/hannahtinkler)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hannahtinkler "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://blog.morph027.de/"><img src="https://avatars3.githubusercontent.com/u/600106?v=3?s=110" width="110px;" alt="morph027"/><br /><sub><b>morph027</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=morph027" title="Code">💻</a></td> | [<img src="https://avatars1.githubusercontent.com/u/1086388?v=4" width="110px;"/><br /><sub>Doeke Zanstra</sub>](https://github.com/doekman)<br />[💻](https://github.com/snipe/snipe-it/commits?author=doekman "Code") | [<img src="https://avatars1.githubusercontent.com/u/4325936?v=4" width="110px;"/><br /><sub>Djamon Staal</sub>](https://www.sdhd.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SjamonDaal "Code") | [<img src="https://avatars3.githubusercontent.com/u/12306859?v=4" width="110px;"/><br /><sub>Earl Ramirez</sub>](https://github.com/EarlRamirez)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EarlRamirez "Code") | [<img src="https://avatars2.githubusercontent.com/u/8671456?v=4" width="110px;"/><br /><sub>Richard Ray Thomas</sub>](https://github.com/RichardRay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=RichardRay "Code") | [<img src="https://avatars3.githubusercontent.com/u/1852688?v=4" width="110px;"/><br /><sub>Ryan Kuba</sub>](https://www.taisun.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thelamer "Code") | [<img src="https://avatars1.githubusercontent.com/u/6751928?v=4" width="110px;"/><br /><sub>Brian Monroe</sub>](https://github.com/ParadoxGuitarist)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist "Code") | [<img src="https://avatars1.githubusercontent.com/u/605167?v=4" width="110px;"/><br /><sub>plexorama</sub>](https://github.com/plexorama)<br />[💻](https://github.com/snipe/snipe-it/commits?author=plexorama "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fvleminckx"><img src="https://avatars3.githubusercontent.com/u/22935755?v=3?s=110" width="110px;" alt="fvleminckx"/><br /><sub><b>fvleminckx</b></sub></a><br /><a href="#infra-fvleminckx" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> | [<img src="https://avatars2.githubusercontent.com/u/1795149?v=4" width="110px;"/><br /><sub>Till Deeke</sub>](https://tilldeeke.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tilldeeke "Code") | [<img src="https://avatars0.githubusercontent.com/u/12634129?v=4" width="110px;"/><br /><sub>5quirrel</sub>](https://github.com/5quirrel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=5quirrel "Code") | [<img src="https://avatars1.githubusercontent.com/u/13071957?v=4" width="110px;"/><br /><sub>Jason</sub>](https://github.com/jasonlshelton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonlshelton "Code") | [<img src="https://avatars3.githubusercontent.com/u/7128321?v=4" width="110px;"/><br /><sub>Antti</sub>](https://github.com/chemfy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chemfy "Code") | [<img src="https://avatars3.githubusercontent.com/u/10080364?v=4" width="110px;"/><br /><sub>DeusMaximus</sub>](https://github.com/DeusMaximus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DeusMaximus "Code") | [<img src="https://avatars2.githubusercontent.com/u/16384611?v=4" width="110px;"/><br /><sub>a-royal</sub>](https://github.com/A-ROYAL)<br />[🌍](#translation-A-ROYAL "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5358208?v=4" width="110px;"/><br /><sub>Alberto Aldrigo</sub>](https://github.com/albertoaldrigo)<br />[🌍](#translation-albertoaldrigo "Translation") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itsupportcmsukorg"><img src="https://avatars2.githubusercontent.com/u/15633547?v=3?s=110" width="110px;" alt="itsupportcmsukorg"/><br /><sub><b>itsupportcmsukorg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg" title="Bug reports">🐛</a></td> | [<img src="https://avatars0.githubusercontent.com/u/1412342?v=4" width="110px;"/><br /><sub>Alex Stanev</sub>](http://alex.stanev.org/blog)<br />[🌍](#translation-RealEnder "Translation") | [<img src="https://avatars0.githubusercontent.com/u/177295?v=4" width="110px;"/><br /><sub>Andreas Rehm</sub>](http://devel.itsolution2.de)<br />[🌍](#translation-sirrus "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5080535?v=4" width="110px;"/><br /><sub>Andreas Erhard</sub>](https://github.com/xelan)<br />[🌍](#translation-xelan "Translation") | [<img src="https://avatars2.githubusercontent.com/u/142350?v=4" width="110px;"/><br /><sub>Andrés Vanegas Jiménez</sub>](https://github.com/angeldeejay)<br />[🌍](#translation-angeldeejay "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3910403?v=4" width="110px;"/><br /><sub>Antonio Schiavon</sub>](https://github.com/aschiavon91)<br />[🌍](#translation-aschiavon91 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10464547?v=4" width="110px;"/><br /><sub>benunter</sub>](https://github.com/benunter)<br />[🌍](#translation-benunter "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5038647?v=4" width="110px;"/><br /><sub>Borys Żmuda</sub>](http://catweb24.pl)<br />[🌍](#translation-rudashi "Translation") |
<td align="center" valign="top" width="14.28%"><a href="https://override.io"><img src="https://avatars3.githubusercontent.com/u/12373799?v=3?s=110" width="110px;" alt="Frank"/><br /><sub><b>Frank</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=base-zero" title="Code">💻</a></td> | [<img src="https://avatars0.githubusercontent.com/u/5539359?v=4" width="110px;"/><br /><sub>chibacityblues</sub>](https://github.com/chibacityblues)<br />[🌍](#translation-chibacityblues "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1954830?v=4" width="110px;"/><br /><sub>Chien Wei Lin</sub>](https://github.com/cwlin0416)<br />[🌍](#translation-cwlin0416 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/11700533?v=4" width="110px;"/><br /><sub>Christian Schuster</sub>](https://github.com/Againstreality)<br />[🌍](#translation-Againstreality "Translation") | [<img src="https://avatars1.githubusercontent.com/u/4308704?v=4" width="110px;"/><br /><sub>Christian Stefanus</sub>](http://chriss.webhostid.com)<br />[🌍](#translation-kopi-item "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3009327?v=4" width="110px;"/><br /><sub>wxcafé</sub>](http://wxcafe.net)<br />[🌍](#translation-wxcafe "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35761525?v=4" width="110px;"/><br /><sub>dpyroc</sub>](https://github.com/dpyroc)<br />[🌍](#translation-dpyroc "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2153639?v=4" width="110px;"/><br /><sub>Daniel Friedlmaier</sub>](http://www.friedlmaier.net)<br />[🌍](#translation-da-friedl "Translation") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ghost"><img src="https://avatars0.githubusercontent.com/u/10137?v=3?s=110" width="110px;" alt="Deleted user"/><br /><sub><b>Deleted user</b></sub></a><br /><a href="#translation-ghost" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=ghost" title="Code">💻</a></td> | [<img src="https://avatars1.githubusercontent.com/u/2947640?v=4" width="110px;"/><br /><sub>Daniel Heene</sub>](https://github.com/danielheene)<br />[🌍](#translation-danielheene "Translation") | [<img src="https://avatars3.githubusercontent.com/u/319022?v=4" width="110px;"/><br /><sub>danielcb</sub>](https://github.com/danielcb)<br />[🌍](#translation-danielcb "Translation") | [<img src="https://avatars3.githubusercontent.com/u/15846537?v=4" width="110px;"/><br /><sub>Dominik Senti</sub>](https://github.com/dominiksenti)<br />[🌍](#translation-dominiksenti "Translation") | [<img src="https://avatars0.githubusercontent.com/u/25570954?v=4" width="110px;"/><br /><sub>Eric Gautheron</sub>](http://www.konectik.com)<br />[🌍](#translation-EpixFr "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5732623?v=4" width="110px;"/><br /><sub>Erlend Pilø</sub>](https://erlpil.com)<br />[🌍](#translation-Erlpil "Translation") | [<img src="https://avatars0.githubusercontent.com/u/541832?v=4" width="110px;"/><br /><sub>Fabio Rapposelli</sub>](http://fabio.technology)<br />[🌍](#translation-frapposelli "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3605240?v=4" width="110px;"/><br /><sub>Felipe Barros</sub>](https://github.com/fgbs)<br />[🌍](#translation-fgbs "Translation") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tiagom62"><img src="https://avatars1.githubusercontent.com/u/10802313?v=3?s=110" width="110px;" alt="tiagom62"/><br /><sub><b>tiagom62</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tiagom62" title="Code">💻</a> <a href="#infra-tiagom62" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> | [<img src="https://avatars0.githubusercontent.com/u/257745?v=4" width="110px;"/><br /><sub>Fernando Possebon</sub>](https://github.com/possebon)<br />[🌍](#translation-possebon "Translation") | [<img src="https://avatars3.githubusercontent.com/u/2540832?v=4" width="110px;"/><br /><sub>gdraque</sub>](https://github.com/gdraque)<br />[🌍](#translation-gdraque "Translation") | [<img src="https://avatars0.githubusercontent.com/u/23440381?v=4" width="110px;"/><br /><sub>Georg Wallisch</sub>](https://github.com/georgwallisch)<br />[🌍](#translation-georgwallisch "Translation") | [<img src="https://avatars1.githubusercontent.com/u/9852832?v=4" width="110px;"/><br /><sub>Gerardo Robles</sub>](https://github.com/jgroblesr85)<br />[🌍](#translation-jgroblesr85 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/11082640?v=4" width="110px;"/><br /><sub>Gluek</sub>](https://t.me/Gluek)<br />[🌍](#translation-mrgluek "Translation") | [<img src="https://avatars0.githubusercontent.com/u/6847946?v=4" width="110px;"/><br /><sub>AdnanAbuShahad</sub>](https://github.com/AdnanAbuShahad)<br />[🌍](#translation-AdnanAbuShahad "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3580608?v=4" width="110px;"/><br /><sub>Hafidzi My</sub>](https://hafidzi.my)<br />[🌍](#translation-hafidzi "Translation") |
</tr> | [<img src="https://avatars2.githubusercontent.com/u/205521?v=4" width="110px;"/><br /><sub>Harim Park</sub>](https://github.com/fofwisdom)<br />[🌍](#translation-fofwisdom "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3333841?v=4" width="110px;"/><br /><sub>Henrik Kentsson</sub>](http://www.kentsson.se)<br />[🌍](#translation-Kentsson "Translation") | [<img src="https://avatars0.githubusercontent.com/u/36551034?v=4" width="110px;"/><br /><sub>Husnul Yaqien</sub>](https://github.com/husnulyaqien)<br />[🌍](#translation-husnulyaqien "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2372747?v=4" width="110px;"/><br /><sub>Ibrahim</sub>](http://abaalkhail.org)<br />[🌍](#translation-abaalkh "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1389334?v=4" width="110px;"/><br /><sub>igolman</sub>](https://github.com/igolman)<br />[🌍](#translation-igolman "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3257070?v=4" width="110px;"/><br /><sub>itangiang</sub>](https://github.com/itangiang)<br />[🌍](#translation-itangiang "Translation") | [<img src="https://avatars2.githubusercontent.com/u/14814254?v=4" width="110px;"/><br /><sub>jarby1211</sub>](https://github.com/jarby1211)<br />[🌍](#translation-jarby1211 "Translation") |
<tr> | [<img src="https://avatars3.githubusercontent.com/u/6719357?v=4" width="110px;"/><br /><sub>Jhonn Willker</sub>](http://jwillker.com)<br />[🌍](#translation-JohnWillker "Translation") | [<img src="https://avatars2.githubusercontent.com/u/10983635?v=4" width="110px;"/><br /><sub>Jose</sub>](https://github.com/joxelito94)<br />[🌍](#translation-joxelito94 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5206122?v=4" width="110px;"/><br /><sub>laopangzi</sub>](https://github.com/laopangzi)<br />[🌍](#translation-laopangzi "Translation") | [<img src="https://avatars2.githubusercontent.com/u/79707?v=4" width="110px;"/><br /><sub>Lars Strojny</sub>](http://usrportage.de)<br />[🌍](#translation-lstrojny "Translation") | [<img src="https://avatars0.githubusercontent.com/u/389801?v=4" width="110px;"/><br /><sub>MarcosBL</sub>](http://twitter.com/marcosbl)<br />[🌍](#translation-MarcosBL "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35664606?v=4" width="110px;"/><br /><sub>marie joy cajes</sub>](https://github.com/mariejoyacajes)<br />[🌍](#translation-mariejoyacajes "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3052816?v=4" width="110px;"/><br /><sub>Mark S. Johansen</sub>](http://www.markjohansen.dk)<br />[🌍](#translation-msjohansen "Translation") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rystaf"><img src="https://avatars3.githubusercontent.com/u/2389047?v=3?s=110" width="110px;" alt="Ryan Stafford"/><br /><sub><b>Ryan Stafford</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rystaf" title="Code">💻</a></td> | [<img src="https://avatars2.githubusercontent.com/u/982885?v=4" width="110px;"/><br /><sub>Martin Stub</sub>](http://martinstub.dk)<br />[🌍](#translation-stubben "Translation") | [<img src="https://avatars2.githubusercontent.com/u/28959963?v=4" width="110px;"/><br /><sub>Meyer Flavio</sub>](https://github.com/meyerf99)<br />[🌍](#translation-meyerf99 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/796443?v=4" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[🌍](#translation-MicaelRodrigues "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10481331?v=4" width="110px;"/><br /><sub>Mikael Rasmussen</sub>](http://rubixy.com/)<br />[🌍](#translation-mikaelssen "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1544552?v=4" width="110px;"/><br /><sub>IxFail</sub>](https://github.com/IxFail)<br />[🌍](#translation-IxFail "Translation") | [<img src="https://avatars3.githubusercontent.com/u/18483118?v=4" width="110px;"/><br /><sub>Mohammed Fota</sub>](http://www.mohammedfota.com)<br />[🌍](#translation-MohammedFota "Translation") | [<img src="https://avatars0.githubusercontent.com/u/227080?v=4" width="110px;"/><br /><sub>Moayad Alserihi</sub>](https://github.com/omego)<br />[🌍](#translation-omego "Translation") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehanlon"><img src="https://avatars2.githubusercontent.com/u/10345935?v=3?s=110" width="110px;" alt="Eammon Hanlon"/><br /><sub><b>Eammon Hanlon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ehanlon" title="Code">💻</a></td> | [<img src="https://avatars0.githubusercontent.com/u/1680266?v=4" width="110px;"/><br /><sub>saymd</sub>](https://github.com/saymd)<br />[🌍](#translation-saymd "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1826808?v=4" width="110px;"/><br /><sub>Patrik Larsson</sub>](https://nordsken.se)<br />[🌍](#translation-pooot "Translation") | [<img src="https://avatars1.githubusercontent.com/u/20584746?v=4" width="110px;"/><br /><sub>drcryo</sub>](https://github.com/drcryo)<br />[🌍](#translation-drcryo "Translation") | [<img src="https://avatars1.githubusercontent.com/u/19408004?v=4" width="110px;"/><br /><sub>pawel1615</sub>](https://github.com/pawel1615)<br />[🌍](#translation-pawel1615 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/23340468?v=4" width="110px;"/><br /><sub>bodrovics</sub>](https://github.com/bodrovics)<br />[🌍](#translation-bodrovics "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3257654?v=4" width="110px;"/><br /><sub>priatna</sub>](https://github.com/priatna)<br />[🌍](#translation-priatna "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5358374?v=4" width="110px;"/><br /><sub>Fan Jiang</sub>](https://amayume.net)<br />[🌍](#translation-ProfFan "Translation") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zjean"><img src="https://avatars0.githubusercontent.com/u/441924?v=3?s=110" width="110px;" alt="zjean"/><br /><sub><b>zjean</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zjean" title="Code">💻</a></td> | [<img src="https://avatars1.githubusercontent.com/u/22555451?v=4" width="110px;"/><br /><sub>ragnarcx</sub>](https://github.com/ragnarcx)<br />[🌍](#translation-ragnarcx "Translation") | [<img src="https://avatars2.githubusercontent.com/u/18654582?v=4" width="110px;"/><br /><sub>Rein van Haaren</sub>](http://www.reinvanhaaren.nl/)<br />[🌍](#translation-reinvanhaaren "Translation") | [<img src="https://avatars1.githubusercontent.com/u/386672?v=4" width="110px;"/><br /><sub>Teguh Dwicaksana</sub>](http://dheche.songolimo.net)<br />[🌍](#translation-dheche "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2572552?v=4" width="110px;"/><br /><sub>fraccie</sub>](https://github.com/FRaccie)<br />[🌍](#translation-FRaccie "Translation") | [<img src="https://avatars0.githubusercontent.com/u/35182720?v=4" width="110px;"/><br /><sub>vinzruzell</sub>](https://github.com/vinzruzell)<br />[🌍](#translation-vinzruzell "Translation") | [<img src="https://avatars1.githubusercontent.com/u/7883603?v=4" width="110px;"/><br /><sub>Kevin Austin</sub>](http://kevinaustin.com)<br />[🌍](#translation-vipsystem "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3861828?v=4" width="110px;"/><br /><sub>Wira Sandy</sub>](http://azuraweb.xyz)<br />[🌍](#translation-wira-sandy "Translation") |
<td align="center" valign="top" width="14.28%"><a href="http://www.frei.media"><img src="https://avatars0.githubusercontent.com/u/12660103?v=3?s=110" width="110px;" alt="Matthias Frei"/><br /><sub><b>Matthias Frei</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FREImedia" title="Code">💻</a></td> | [<img src="https://avatars2.githubusercontent.com/u/8663789?v=4" width="110px;"/><br /><sub>Илья</sub>](https://github.com/GrayHoax)<br />[🌍](#translation-GrayHoax "Translation") | [<img src="https://avatars3.githubusercontent.com/u/30119111?v=4" width="110px;"/><br /><sub>GodUseVPN</sub>](https://github.com/godusevpn)<br />[🌍](#translation-godusevpn "Translation") | [<img src="https://avatars1.githubusercontent.com/u/745576?v=4" width="110px;"/><br /><sub>周周</sub>](https://github.com/EngrZhou)<br />[🌍](#translation-EngrZhou "Translation") | [<img src="https://avatars3.githubusercontent.com/u/1631095?v=4" width="110px;"/><br /><sub>Sam</sub>](https://github.com/takuy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=takuy "Code") | [<img src="https://avatars1.githubusercontent.com/u/264022?v=4" width="110px;"/><br /><sub>Azerothian</sub>](https://www.illisian.com.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azerothian "Code") | [<img src="https://avatars1.githubusercontent.com/u/4930051?v=4" width="110px;"/><br /><sub>Wes Hulette</sub>](http://macfoo.wordpress.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jwhulette "Code") | [<img src="https://avatars0.githubusercontent.com/u/8134591?v=4" width="110px;"/><br /><sub>patrict</sub>](https://github.com/patrict)<br />[💻](https://github.com/snipe/snipe-it/commits?author=patrict "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/opsydev"><img src="https://avatars0.githubusercontent.com/u/3767518?v=3?s=110" width="110px;" alt="opsydev"/><br /><sub><b>opsydev</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=opsydev" title="Code">💻</a></td> | [<img src="https://avatars3.githubusercontent.com/u/2611616?v=4" width="110px;"/><br /><sub>Dmitriy Minaev</sub>](https://github.com/VELIKII-DIVAN)<br />[💻](https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN "Code") | [<img src="https://avatars0.githubusercontent.com/u/5132245?v=4" width="110px;"/><br /><sub>liquidhorse</sub>](https://github.com/liquidhorse)<br />[💻](https://github.com/snipe/snipe-it/commits?author=liquidhorse "Code") | [<img src="https://avatars1.githubusercontent.com/u/183678?v=4" width="110px;"/><br /><sub>Jordi Boggiano</sub>](https://seld.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Seldaek "Code") | [<img src="https://avatars0.githubusercontent.com/u/653557?v=4" width="110px;"/><br /><sub>Ivan Nieto</sub>](https://github.com/inietov)<br />[💻](https://github.com/snipe/snipe-it/commits?author=inietov "Code") | [<img src="https://avatars2.githubusercontent.com/u/6764151?v=4" width="110px;"/><br /><sub>Ben RUBSON</sub>](https://github.com/benrubson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benrubson "Code") | [<img src="https://avatars2.githubusercontent.com/u/8554558?v=4" width="110px;"/><br /><sub>NMathar</sub>](https://github.com/NMathar)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NMathar "Code") | [<img src="https://avatars1.githubusercontent.com/u/139566?v=4" width="110px;"/><br /><sub>Steffen</sub>](https://github.com/smb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smb "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://www.ddreier.com"><img src="https://avatars1.githubusercontent.com/u/82290?v=3?s=110" width="110px;" alt="Daniel Dreier"/><br /><sub><b>Daniel Dreier</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ddreier" title="Code">💻</a></td> | [<img src="https://avatars0.githubusercontent.com/u/6609453?v=4" width="110px;"/><br /><sub>Sxderp</sub>](https://github.com/Sxderp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Sxderp "Code") | [<img src="https://avatars1.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>fanta8897</sub>](https://github.com/fanta8897)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fanta8897 "Code") | [<img src="https://avatars2.githubusercontent.com/u/2576509?v=4" width="110px;"/><br /><sub>Andrey Bolonin</sub>](https://andreybolonin.com/phpconsulting/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreybolonin "Code") | [<img src="https://avatars3.githubusercontent.com/u/2173307?v=4" width="110px;"/><br /><sub>shinayoshi</sub>](http://www.shinayoshi.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=shinayoshi "Code") | [<img src="https://avatars3.githubusercontent.com/u/2130159?v=4" width="110px;"/><br /><sub>Hubert</sub>](https://github.com/reuser)<br />[💻](https://github.com/snipe/snipe-it/commits?author=reuser "Code") | [<img src="https://avatars0.githubusercontent.com/u/6865789?v=4" width="110px;"/><br /><sub>KeenRivals</sub>](https://brashear.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KeenRivals "Code") | [<img src="https://avatars3.githubusercontent.com/u/2902513?v=4" width="110px;"/><br /><sub>omyno</sub>](https://github.com/omyno)<br />[💻](https://github.com/snipe/snipe-it/commits?author=omyno "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://rassie.org"><img src="https://avatars0.githubusercontent.com/u/23448?v=3?s=110" width="110px;" alt="Nikolai Prokoschenko"/><br /><sub><b>Nikolai Prokoschenko</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rassie" title="Code">💻</a></td> | [<img src="https://avatars1.githubusercontent.com/u/6271335?v=4" width="110px;"/><br /><sub>Evgeny</sub>](https://github.com/jackka)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jackka "Code") | [<img src="https://avatars2.githubusercontent.com/u/1169963?v=4" width="110px;"/><br /><sub>Colin Campbell</sub>](https://digitalist.se)<br />[💻](https://github.com/snipe/snipe-it/commits?author=colin-campbell "Code") | [<img src="https://avatars3.githubusercontent.com/u/2872098?v=4" width="110px;"/><br /><sub>Ľubomír Kučera</sub>](https://github.com/lubo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lubo "Code") | [<img src="https://avatars3.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://www.sourceguru.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mezzle "Code") | [<img src="https://avatars1.githubusercontent.com/u/7632599?v=4" width="110px;"/><br /><sub>Tim Farmer</sub>](https://github.com/timothyfarmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timothyfarmer "Code") | [<img src="https://avatars0.githubusercontent.com/u/17459600?v=4" width="110px;"/><br /><sub>Marián Skrip</sub>](https://github.com/mskrip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mskrip "Code") | [<img src="https://avatars2.githubusercontent.com/u/47435081?v=4" width="110px;"/><br /><sub>Godfrey Martinez</sub>](https://github.com/Godmartinz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Godmartinz "Code") |
</tr> | [<img src="https://avatars1.githubusercontent.com/u/2075128?v=4" width="110px;"/><br /><sub>bigtreeEdo</sub>](https://github.com/bigtreeEdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bigtreeEdo "Code") | [<img src="https://avatars0.githubusercontent.com/u/5000430?v=4" width="110px;"/><br /><sub>Colin McNeil</sub>](https://colinmcneil.me/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ColinMcNeil "Code") | [<img src="https://avatars0.githubusercontent.com/u/421625?v=4" width="110px;"/><br /><sub>JoKneeMo</sub>](https://github.com/JoKneeMo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JoKneeMo "Code") | [<img src="https://avatars0.githubusercontent.com/u/54849013?v=4" width="110px;"/><br /><sub>Joshi</sub>](http://www.redbridge.se)<br />[💻](https://github.com/snipe/snipe-it/commits?author=joshi-redbridge "Code") | [<img src="https://avatars2.githubusercontent.com/u/15731458?v=4" width="110px;"/><br /><sub>Anthony Burns</sub>](https://github.com/anthonypburns)<br />[💻](https://github.com/snipe/snipe-it/commits?author=anthonypburns "Code") | [<img src="https://avatars1.githubusercontent.com/u/63399474?v=4" width="110px;"/><br /><sub>johnson-yi</sub>](https://github.com/johnson-yi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=johnson-yi "Code") | [<img src="https://avatars1.githubusercontent.com/u/1862720?v=4" width="110px;"/><br /><sub>Sanjay Govind</sub>](https://tangentmc.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sanjay900 "Code") |
<tr> | [<img src="https://avatars0.githubusercontent.com/u/1255375?v=4" width="110px;"/><br /><sub>Peter Upfold</sub>](https://peter.upfold.org.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PeterUpfold "Code") | [<img src="https://avatars2.githubusercontent.com/u/961717?v=4" width="110px;"/><br /><sub>Jared Biel</sub>](https://github.com/jbiel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jbiel "Code") | [<img src="https://avatars1.githubusercontent.com/u/1733625?v=4" width="110px;"/><br /><sub>Dampfklon</sub>](https://github.com/dampfklon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dampfklon "Code") | [<img src="https://avatars2.githubusercontent.com/u/52973156?v=4" width="110px;"/><br /><sub>Charles Hamilton</sub>](https://communityclosing.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chamilton-ccn "Code") | [<img src="https://avatars.githubusercontent.com/u/551789?v=4" width="110px;"/><br /><sub>Giuseppe Iannello</sub>](https://github.com/giannello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=giannello "Code") | [<img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="110px;"/><br /><sub>Peter Dave Hello</sub>](https://www.peterdavehello.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PeterDaveHello "Code") | [<img src="https://avatars.githubusercontent.com/u/6106332?v=4" width="110px;"/><br /><sub>sigmoidal</sub>](https://github.com/sigmoidal)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sigmoidal "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YetAnotherCodeMonkey"><img src="https://avatars0.githubusercontent.com/u/13452757?v=3?s=110" width="110px;" alt="Drew"/><br /><sub><b>Drew</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/2082554?v=4" width="110px;"/><br /><sub>Vincent Lainé</sub>](https://github.com/phenixdotnet)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phenixdotnet "Code") | [<img src="https://avatars.githubusercontent.com/u/1943040?v=4" width="110px;"/><br /><sub>Lucas Pleß</sub>](http://www.lucas-pless.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derlucas "Code") | [<img src="https://avatars.githubusercontent.com/u/472804?v=4" width="110px;"/><br /><sub>Ian Littman</sub>](http://twitter.com/iansltx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=iansltx "Code") | [<img src="https://avatars.githubusercontent.com/u/3519029?v=4" width="110px;"/><br /><sub>João Paulo</sub>](https://github.com/PauloLuna)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PauloLuna "Code") | [<img src="https://avatars.githubusercontent.com/u/70443365?v=4" width="110px;"/><br /><sub>ThoBur</sub>](https://github.com/ThoBur)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ThoBur "Code") | [<img src="https://avatars.githubusercontent.com/u/1972329?v=4" width="110px;"/><br /><sub>Alexander Chibrikin</sub>](http://phpprofi.ru/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alek13 "Code") | [<img src="https://avatars.githubusercontent.com/u/438332?v=4" width="110px;"/><br /><sub>Anthony Winstanley</sub>](https://github.com/winstan)<br />[💻](https://github.com/snipe/snipe-it/commits?author=winstan "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/merid14"><img src="https://avatars0.githubusercontent.com/u/1342320?v=3?s=110" width="110px;" alt="Walter"/><br /><sub><b>Walter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=merid14" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/3075214?v=4" width="110px;"/><br /><sub>Folke</sub>](https://github.com/fashberg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fashberg "Code") | [<img src="https://avatars.githubusercontent.com/u/1351571?v=4" width="110px;"/><br /><sub>Bennett Blodinger</sub>](https://github.com/benwa)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benwa "Code") | [<img src="https://avatars.githubusercontent.com/u/2974631?v=4" width="110px;"/><br /><sub>NMC</sub>](https://nmc.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ncareau "Code") | [<img src="https://avatars.githubusercontent.com/u/52182449?v=4" width="110px;"/><br /><sub>andres-baller</sub>](https://github.com/andres-baller)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andres-baller "Code") | [<img src="https://avatars.githubusercontent.com/u/67109348?v=4" width="110px;"/><br /><sub>sean-borg</sub>](https://github.com/sean-borg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sean-borg "Code") | [<img src="https://avatars.githubusercontent.com/u/32170051?v=4" width="110px;"/><br /><sub>EDVLeer</sub>](https://github.com/EDVLeer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EDVLeer "Code") | [<img src="https://avatars.githubusercontent.com/u/23075196?v=4" width="110px;"/><br /><sub>Kurokat</sub>](https://github.com/Kurokat)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Kurokat "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/balous"><img src="https://avatars3.githubusercontent.com/u/11254614?v=3?s=110" width="110px;" alt="Petr Baloun"/><br /><sub><b>Petr Baloun</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=balous" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/915514?v=4" width="110px;"/><br /><sub>Kevin Köllmann</sub>](https://www.kevinkoellmann.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koelle25 "Code") | [<img src="https://avatars.githubusercontent.com/u/49025941?v=4" width="110px;"/><br /><sub>sw-mreyes</sub>](https://github.com/sw-mreyes)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sw-mreyes "Code") | [<img src="https://avatars.githubusercontent.com/u/70129?v=4" width="110px;"/><br /><sub>Joel Pittet</sub>](https://pittet.ca)<br />[💻](https://github.com/snipe/snipe-it/commits?author=joelpittet "Code") | [<img src="https://avatars.githubusercontent.com/u/792695?v=4" width="110px;"/><br /><sub>Eli Young</sub>](https://elyscape.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=elyscape "Code") | [<img src="https://avatars.githubusercontent.com/u/317015?v=4" width="110px;"/><br /><sub>Raell Dottin</sub>](https://github.com/raelldottin)<br />[💻](https://github.com/snipe/snipe-it/commits?author=raelldottin "Code") | [<img src="https://avatars.githubusercontent.com/u/1446856?v=4" width="110px;"/><br /><sub>Tom Misilo</sub>](https://github.com/misilot)<br />[💻](https://github.com/snipe/snipe-it/commits?author=misilot "Code") | [<img src="https://avatars.githubusercontent.com/u/4496300?v=4" width="110px;"/><br /><sub>David Davenne</sub>](http://david.davenne.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JuustoMestari "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reidblomquist"><img src="https://avatars0.githubusercontent.com/u/6117660?v=3?s=110" width="110px;" alt="reidblomquist"/><br /><sub><b>reidblomquist</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=reidblomquist" title="Documentation">📖</a></td> | [<img src="https://avatars.githubusercontent.com/u/9255772?v=4" width="110px;"/><br /><sub>Mark Stenglein</sub>](https://markstenglein.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ocelotsloth "Code") | [<img src="https://avatars.githubusercontent.com/u/35658596?v=4" width="110px;"/><br /><sub>ajsy</sub>](https://github.com/ajsy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ajsy "Code") | [<img src="https://avatars.githubusercontent.com/u/3628035?v=4" width="110px;"/><br /><sub>Jan Kiesewetter</sub>](https://github.com/t3easy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=t3easy "Code") | [<img src="https://avatars.githubusercontent.com/u/79449630?v=4" width="110px;"/><br /><sub>Tetrachloromethane250</sub>](https://github.com/Tetrachloromethane250)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250 "Code") | [<img src="https://avatars.githubusercontent.com/u/22004482?v=4" width="110px;"/><br /><sub>Lars Kajes</sub>](https://www.kajes.se/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kajes "Code") | [<img src="https://avatars.githubusercontent.com/u/13993216?v=4" width="110px;"/><br /><sub>Joly0</sub>](https://github.com/Joly0)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Joly0 "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>theburger</sub>](https://github.com/limeless)<br />[💻](https://github.com/snipe/snipe-it/commits?author=limeless "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mathieuk"><img src="https://avatars0.githubusercontent.com/u/539914?v=3?s=110" width="110px;" alt="Mathieu Kooiman"/><br /><sub><b>Mathieu Kooiman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mathieuk" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/36065681?v=4" width="110px;"/><br /><sub>David Valin Alonso</sub>](https://github.com/deivishome)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deivishome "Code") | [<img src="https://avatars.githubusercontent.com/u/8290389?v=4" width="110px;"/><br /><sub>andreaci</sub>](https://github.com/andreaci)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreaci "Code") | [<img src="https://avatars.githubusercontent.com/u/1828542?v=4" width="110px;"/><br /><sub>Jelle Sebreghts</sub>](http://www.jellesebreghts.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jelle-S "Code") | [<img src="https://avatars.githubusercontent.com/u/11180862?v=4" width="110px;"/><br /><sub>Michael Pietsch</sub>](https://github.com/Skywalker-11)<br /> | [<img src="https://avatars.githubusercontent.com/u/22068886?v=4" width="110px;"/><br /><sub>Masudul Haque Shihab</sub>](https://github.com/sh1hab)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sh1hab "Code") | [<img src="https://avatars.githubusercontent.com/u/16099942?v=4" width="110px;"/><br /><sub>Supapong Areeprasertkul</sub>](http://www.freedomdive.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zybersup "Code") | [<img src="https://avatars.githubusercontent.com/u/207358?v=4" width="110px;"/><br /><sub>Peter Sarossy</sub>](https://github.com/psarossy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=psarossy "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csayre"><img src="https://avatars3.githubusercontent.com/u/6606421?v=3?s=110" width="110px;" alt="csayre"/><br /><sub><b>csayre</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=csayre" title="Documentation">📖</a></td> | [<img src="https://avatars.githubusercontent.com/u/11823649?v=4" width="110px;"/><br /><sub>Renee Margaret McConahy</sub>](https://github.com/nepella)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nepella "Code") | [<img src="https://avatars.githubusercontent.com/u/5553884?v=4" width="110px;"/><br /><sub>JohnnyPicnic</sub>](https://github.com/JohnnyPicnic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic "Code") | [<img src="https://avatars.githubusercontent.com/u/8799594?v=4" width="110px;"/><br /><sub>markbrule</sub>](https://github.com/markbrule)<br />[💻](https://github.com/snipe/snipe-it/commits?author=markbrule "Code") | [<img src="https://avatars.githubusercontent.com/u/1962801?v=4" width="110px;"/><br /><sub>Mike Campbell</sub>](https://github.com/mikecmpbll)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikecmpbll "Code") | [<img src="https://avatars.githubusercontent.com/u/11973217?v=4" width="110px;"/><br /><sub>tbrconnect</sub>](https://github.com/tbrconnect)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tbrconnect "Code") | [<img src="https://avatars.githubusercontent.com/u/12447225?v=4" width="110px;"/><br /><sub>kcoyo</sub>](https://github.com/kcoyo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kcoyo "Code") | [<img src="https://avatars.githubusercontent.com/u/494017?v=4" width="110px;"/><br /><sub>Travis Miller</sub>](https://travismiller.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=travismiller "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/adamdunson"><img src="https://avatars1.githubusercontent.com/u/768488?v=3?s=110" width="110px;" alt="Adam Dunson"/><br /><sub><b>Adam Dunson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adamdunson" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/1975640?v=4" width="110px;"/><br /><sub>Evan Taylor</sub>](https://github.com/Delta5)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Delta5 "Code") | [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") |
</tr> | [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") | [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/24418301?v=4" width="110px;"/><br /><sub>LEITWERK AG</sub>](https://www.leitwerk.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leitwerk-ag "Code") |
<tr> | [<img src="https://avatars.githubusercontent.com/u/1911435?v=4" width="110px;"/><br /><sub>Adam</sub>](http://www.aboutcher.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamboutcher "Code") | [<img src="https://avatars.githubusercontent.com/u/16104273?v=4" width="110px;"/><br /><sub>Ian</sub>](https://snksrv.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sneak-it "Code") | [<img src="https://avatars.githubusercontent.com/u/4023909?v=4" width="110px;"/><br /><sub>Shao Yu-Lung (Allen)</sub>](http://blog.bestlong.idv.tw/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bestlong "Code") | [<img src="https://avatars.githubusercontent.com/u/76475453?v=4" width="110px;"/><br /><sub>Haxatron</sub>](https://github.com/Haxatron)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Haxatron "Code") | [<img src="https://avatars.githubusercontent.com/u/88776392?v=4" width="110px;"/><br /><sub>PlaneNuts</sub>](https://github.com/PlaneNuts)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PlaneNuts "Code") | [<img src="https://avatars.githubusercontent.com/u/3842948?v=4" width="110px;"/><br /><sub>Bradley Coudriet</sub>](http://bjcpgd.cias.rit.edu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=exula "Code") | [<img src="https://avatars.githubusercontent.com/u/21966173?v=4" width="110px;"/><br /><sub>Dalton Durst</sub>](https://daltondur.st)<br />[💻](https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/thehereward"><img src="https://avatars0.githubusercontent.com/u/5547470?v=3?s=110" width="110px;" alt="Hereward"/><br /><sub><b>Hereward</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thehereward" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/38761237?v=4" width="110px;"/><br /><sub>Alex Janes</sub>](https://adagiohealth.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [<img src="https://avatars.githubusercontent.com/u/32387849?v=4" width="110px;"/><br /><sub>Nuraeil</sub>](https://github.com/nuraeil)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") | [<img src="https://avatars.githubusercontent.com/u/48162670?v=4" width="110px;"/><br /><sub>TenOfTens</sub>](https://github.com/TenOfTens)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TenOfTens "Code") | [<img src="https://avatars.githubusercontent.com/u/9415391?v=4" width="110px;"/><br /><sub>waffle</sub>](https://ditisjens.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=insert-waffle "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/qveensi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qveensi "Code") | [<img src="https://avatars.githubusercontent.com/u/3839381?v=4" width="110px;"/><br /><sub>Achmad Fienan Rahardianto</sub>](https://github.com/veenone)<br />[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swoopdk"><img src="https://avatars0.githubusercontent.com/u/5802977?v=3?s=110" width="110px;" alt="swoopdk"/><br /><sub><b>swoopdk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=swoopdk" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/1294403?v=4" width="110px;"/><br /><sub>denzfarid</sub>](https://github.com/denzfarid)<br /> | [<img src="https://avatars.githubusercontent.com/u/94018771?v=4" width="110px;"/><br /><sub>ntbutler-nbcs</sub>](https://github.com/ntbutler-nbcs)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [<img src="https://avatars.githubusercontent.com/u/172697?v=4" width="110px;"/><br /><sub>Naveen</sub>](https://naveensrinivasan.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [<img src="https://avatars.githubusercontent.com/u/55674383?v=4" width="110px;"/><br /><sub>Mike Roquemore</sub>](https://github.com/mikeroq)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [<img src="https://avatars.githubusercontent.com/u/7991086?v=4" width="110px;"/><br /><sub>Daniel Reeder</sub>](https://github.com/reederda)<br />[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [<img src="https://avatars.githubusercontent.com/u/109422491?v=4" width="110px;"/><br /><sub>vickyjaura183</sub>](https://github.com/vickyjaura183)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") | [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://linkedin.com/in/ahimta"><img src="https://avatars1.githubusercontent.com/u/3470403?v=3?s=110" width="110px;" alt="Abdullah Alansari"/><br /><sub><b>Abdullah Alansari</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Ahimta" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") | [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MicaelRodrigues"><img src="https://avatars0.githubusercontent.com/u/796443?v=3?s=110" width="110px;" alt="Micael Rodrigues"/><br /><sub><b>Micael Rodrigues</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") | [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://macadmincorner.com"><img src="https://avatars0.githubusercontent.com/u/614564?v=3?s=110" width="110px;" alt="Patrick Gallagher"/><br /><sub><b>Patrick Gallagher</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=patgmac" title="Documentation">📖</a></td> | [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") | [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Miliamber"><img src="https://avatars3.githubusercontent.com/u/7165922?v=3?s=110" width="110px;" alt="Miliamber"/><br /><sub><b>Miliamber</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Miliamber" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") | [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hawk554"><img src="https://avatars3.githubusercontent.com/u/861766?v=3?s=110" width="110px;" alt="hawk554"/><br /><sub><b>hawk554</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hawk554" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") | [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") |
</tr> | [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") |
<tr> | [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://jbirdkerr.net"><img src="https://avatars1.githubusercontent.com/u/1695622?v=3?s=110" width="110px;" alt="Justin Kerr"/><br /><sub><b>Justin Kerr</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jbirdkerr" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://www.irasnyder.com/devel/"><img src="https://avatars3.githubusercontent.com/u/11426176?v=3?s=110" width="110px;" alt="Ira W. Snyder"/><br /><sub><b>Ira W. Snyder</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=irasnyd" title="Documentation">📖</a></td> | [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aalaily"><img src="https://avatars2.githubusercontent.com/u/2475759?v=3?s=110" width="110px;" alt="Aladin Alaily"/><br /><sub><b>Aladin Alaily</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=aalaily" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/25596663?v=4" width="110px;"/><br /><sub>aHVzY2g</sub>](https://github.com/aHVzY2g)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [<img src="https://avatars.githubusercontent.com/u/13408130?v=4" width="110px;"/><br /><sub>林博仁 Buo-ren Lin</sub>](https://brlin.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") | [<img src="https://avatars.githubusercontent.com/u/18550946?v=4" width="110px;"/><br /><sub>Adugna Gizaw</sub>](https://orbalia.pythonanywhere.com/)<br />[🌍](#translation-addex12 "Translation") | [<img src="https://avatars.githubusercontent.com/u/760989?v=4" width="110px;"/><br /><sub>Jesse Ostrander</sub>](https://github.com/jostrander)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jostrander "Code") | [<img src="https://avatars.githubusercontent.com/u/31522486?v=4" width="110px;"/><br /><sub>James M</sub>](https://github.com/azmcnutt)<br />[💻](https://github.com/snipe/snipe-it/commits?author=azmcnutt "Code") | [<img src="https://avatars.githubusercontent.com/u/5183146?v=4" width="110px;"/><br /><sub>Fiala06</sub>](https://github.com/Fiala06)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Fiala06 "Code") | [<img src="https://avatars.githubusercontent.com/u/28693782?v=4" width="110px;"/><br /><sub>Nathan Taylor</sub>](https://github.com/ntaylor-86)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntaylor-86 "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kobie-chasehansen"><img src="https://avatars0.githubusercontent.com/u/10247644?v=3?s=110" width="110px;" alt="Chase Hansen"/><br /><sub><b>Chase Hansen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen" title="Code">💻</a> <a href="#question-kobie-chasehansen" title="Answering Questions">💬</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen" title="Bug reports">🐛</a></td> | [<img src="https://avatars.githubusercontent.com/u/16699443?v=4" width="110px;"/><br /><sub>fvollmer</sub>](https://github.com/fvollmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fvollmer "Code") | [<img src="https://avatars.githubusercontent.com/u/109086466?v=4" width="110px;"/><br /><sub>36864</sub>](https://github.com/36864)<br />[💻](https://github.com/snipe/snipe-it/commits?author=36864 "Code") | [<img src="https://avatars.githubusercontent.com/u/365751?v=4" width="110px;"/><br /><sub>Daniel O'Connor</sub>](http://clockwerx.blogspot.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CloCkWeRX "Code") | [<img src="https://avatars.githubusercontent.com/u/102852568?v=4" width="110px;"/><br /><sub>BeatSpark</sub>](https://github.com/BeatSpark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BeatSpark "Code") | [<img src="https://avatars.githubusercontent.com/u/59203607?v=4" width="110px;"/><br /><sub>mrdahbi</sub>](https://github.com/mrdahbi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrdahbi "Code") | [<img src="https://avatars.githubusercontent.com/u/6661332?v=4" width="110px;"/><br /><sub>Fabian Schmid</sub>](http://sr.solutions)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chfsx "Code") | [<img src="https://avatars.githubusercontent.com/u/1288116?v=4" width="110px;"/><br /><sub>Chris Olin</sub>](https://www.chrisolin.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=realchrisolin "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IDM-Helpdesk"><img src="https://avatars2.githubusercontent.com/u/13545400?v=3?s=110" width="110px;" alt="IDM Helpdesk"/><br /><sub><b>IDM Helpdesk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/3803132?v=4" width="110px;"/><br /><sub>Dan</sub>](https://github.com/mnemonicly)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mnemonicly "Code") | [<img src="https://avatars.githubusercontent.com/u/43917728?v=4" width="110px;"/><br /><sub>Nebel</sub>](https://github.com/NebelKreis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NebelKreis "Code") | [<img src="https://avatars.githubusercontent.com/u/132433803?v=4" width="110px;"/><br /><sub>test1337ahp</sub>](https://github.com/test1337ahp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=test1337ahp "Code") | [<img src="https://avatars.githubusercontent.com/u/1916566?v=4" width="110px;"/><br /><sub>Jonathon Reinhart</sub>](https://github.com/JonathonReinhart)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JonathonReinhart "Code") | [<img src="https://avatars.githubusercontent.com/u/484742?v=4" width="110px;"/><br /><sub>aranar-pro</sub>](https://github.com/aranar-pro)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aranar-pro "Code") | [<img src="https://avatars.githubusercontent.com/u/27019397?v=4" width="110px;"/><br /><sub>Phil</sub>](https://github.com/phil-flip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phil-flip "Code") | [<img src="https://avatars.githubusercontent.com/u/6473460?v=4" width="110px;"/><br /><sub>Steffy Fort</sub>](https://fe80.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fe80 "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://balticer.de"><img src="https://avatars2.githubusercontent.com/u/614439?v=3?s=110" width="110px;" alt="Kai"/><br /><sub><b>Kai</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=balticer" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/3302372?v=4" width="110px;"/><br /><sub>Jared Busch</sub>](https://github.com/sorvani)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sorvani "Code") | [<img src="https://avatars.githubusercontent.com/u/111956991?v=4" width="110px;"/><br /><sub>seanborg-codethink</sub>](https://github.com/seanborg-codethink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanborg-codethink "Code") | [<img src="https://avatars.githubusercontent.com/u/160669961?v=4" width="110px;"/><br /><sub>dkaatz</sub>](https://github.com/dkaatz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkaatz "Code") | [<img src="https://avatars.githubusercontent.com/u/827205?v=4" width="110px;"/><br /><sub>Daniel Ruf</sub>](https://threema.id/74SF7MW6?text=)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielRuf "Code") | [<img src="https://avatars.githubusercontent.com/u/38883201?v=4" width="110px;"/><br /><sub>ahpaleus</sub>](https://github.com/ahpaleus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ahpaleus "Code") | [<img src="https://avatars.githubusercontent.com/u/22906055?v=4" width="110px;"/><br /><sub>Anh DAO-DUY</sub>](https://github.com/mink-adao-duy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mink-adao-duy "Code") | [<img src="https://avatars.githubusercontent.com/u/4723453?v=4" width="110px;"/><br /><sub>Andres Gutierrez</sub>](https://github.com/Serdnad)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Serdnad "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://www.michaeldaniels.me"><img src="https://avatars1.githubusercontent.com/u/8762511?v=3?s=110" width="110px;" alt="Michael Daniels"/><br /><sub><b>Michael Daniels</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mdaniels5757" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/111083379?v=4" width="110px;"/><br /><sub>Warren White</sub>](https://github.com/wewhite)<br />[💻](https://github.com/snipe/snipe-it/commits?author=wewhite "Code") | [<img src="https://avatars.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://robintemme.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=robintemme "Code") | [<img src="https://avatars.githubusercontent.com/u/47008367?v=4" width="110px;"/><br /><sub>herroworrd</sub>](https://github.com/herroworrd)<br />[💻](https://github.com/snipe/snipe-it/commits?author=herroworrd "Code") | [<img src="https://avatars.githubusercontent.com/u/28558609?v=4" width="110px;"/><br /><sub>vicleos</sub>](https://mubiu.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vicleos "Code") | [<img src="https://avatars.githubusercontent.com/u/1016780?v=4" width="110px;"/><br /><sub>Bob Clough</sub>](http://thinkl33t.co.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thinkl33t "Code") | [<img src="https://avatars.githubusercontent.com/u/10648463?v=4" width="110px;"/><br /><sub>Brandon Daniel Bailey</sub>](https://github.com/brandon-bailey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brandon-bailey "Code") | [<img src="https://avatars.githubusercontent.com/u/23556080?v=4" width="110px;"/><br /><sub>Marc Bartelt</sub>](https://github.com/marcquark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcquark "Code") |
</tr> | [<img src="https://avatars.githubusercontent.com/u/18286893?v=4" width="110px;"/><br /><sub>manu-crealytics</sub>](https://github.com/manu-crealytics)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manu-crealytics "Code") | [<img src="https://avatars.githubusercontent.com/u/18245993?v=4" width="110px;"/><br /><sub>Konstantin Köhring</sub>](https://www.galaxy102.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Galaxy102 "Code") | [<img src="https://avatars.githubusercontent.com/u/685167?v=4" width="110px;"/><br /><sub>Deloz</sub>](https://deloz.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deloz "Code") | [<img src="https://avatars.githubusercontent.com/u/2682426?v=4" width="110px;"/><br /><sub>Martin Berg</sub>](https://github.com/mbrrg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mbrrg "Code") | [<img src="https://avatars.githubusercontent.com/u/3694534?v=4" width="110px;"/><br /><sub>Richard Schwab</sub>](https://github.com/Nothing4You)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nothing4You "Code") | [<img src="https://avatars.githubusercontent.com/u/8959676?v=4" width="110px;"/><br /><sub>Rick Heil</sub>](https://rickheil.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rickheil "Code") | [<img src="https://avatars.githubusercontent.com/u/397106?v=4" width="110px;"/><br /><sub>Ross Crawford-d'Heureuse</sub>](https://github.com/rosscdh)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rosscdh "Code") |
<tr> | [<img src="https://avatars.githubusercontent.com/u/1621107?v=4" width="110px;"/><br /><sub>Ryan McGuire</sub>](https://github.com/McG800)<br />[💻](https://github.com/snipe/snipe-it/commits?author=McG800 "Code") | [<img src="https://avatars.githubusercontent.com/u/77835667?v=4" width="110px;"/><br /><sub>SBrown2021</sub>](https://github.com/SBrown2021)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SBrown2021 "Code") | [<img src="https://avatars.githubusercontent.com/u/8780913?v=4" width="110px;"/><br /><sub>Serkan</sub>](https://github.com/serkanerip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=serkanerip "Code") | [<img src="https://avatars.githubusercontent.com/u/63188620?v=4" width="110px;"/><br /><sub>Shanks</sub>](https://www.yudelei.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Shankschn "Code") | [<img src="https://avatars.githubusercontent.com/u/198525698?v=4" width="110px;"/><br /><sub>cendai-mis</sub>](https://github.com/cendai-mis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cendai-mis "Code") | [<img src="https://avatars.githubusercontent.com/u/8724583?v=4" width="110px;"/><br /><sub>Shaun McPeck</sub>](https://smcpeck.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smcpeck "Code") | [<img src="https://avatars.githubusercontent.com/u/1378836?v=4" width="110px;"/><br /><sub>Stephen</sub>](https://github.com/snazy2000)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snazy2000 "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://tomcastleman.me"><img src="https://avatars3.githubusercontent.com/u/1532660?v=3?s=110" width="110px;" alt="Tom Castleman"/><br /><sub><b>Tom Castleman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tomcastleman" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/4462739?v=4" width="110px;"/><br /><sub>Steven</sub>](http://nevets82.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nevets82 "Code") | [<img src="https://avatars.githubusercontent.com/u/29017267?v=4" width="110px;"/><br /><sub>Mateus Villar</sub>](https://mateusvillar.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mateus-Romera "Code") | [<img src="https://avatars.githubusercontent.com/u/12749393?v=4" width="110px;"/><br /><sub>Matthew Zackschewski</sub>](https://github.com/mzack5020)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mzack5020 "Code") | [<img src="https://avatars.githubusercontent.com/u/12660103?v=4" width="110px;"/><br /><sub>Matthias Frei</sub>](https://www.frei.media/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=firefrei "Code") | [<img src="https://avatars.githubusercontent.com/u/824840?v=4" width="110px;"/><br /><sub>Nenad Ticaric</sub>](https://github.com/nticaric)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nticaric "Code") | [<img src="https://avatars.githubusercontent.com/u/706439?v=4" width="110px;"/><br /><sub>Nikolay Didenko</sub>](https://github.com/Scorcher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scorcher "Code") | [<img src="https://avatars.githubusercontent.com/u/5457236?v=4" width="110px;"/><br /><sub>Nuno Maduro</sub>](https://nunomaduro.com/sponsorships)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nunomaduro "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DanielNemanic"><img src="https://avatars3.githubusercontent.com/u/10723243?v=3?s=110" width="110px;" alt="Daniel Nemanic"/><br /><sub><b>Daniel Nemanic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DanielNemanic" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/8883074?v=4" width="110px;"/><br /><sub>Oliver Walerys</sub>](https://tektikhq.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=owalerys "Code") | [<img src="https://avatars.githubusercontent.com/u/3102039?v=4" width="110px;"/><br /><sub>R. Christian McDonald</sub>](https://keybase.io/rcmcdonald91)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rcmcdonald91 "Code") | [<img src="https://avatars.githubusercontent.com/u/1525581?v=4" width="110px;"/><br /><sub>nix</sub>](https://nnix.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nixn "Code") | [<img src="https://avatars.githubusercontent.com/u/55462380?v=4" width="110px;"/><br /><sub>octobunny</sub>](https://github.com/octobunny)<br />[💻](https://github.com/snipe/snipe-it/commits?author=octobunny "Code") | [<img src="https://avatars.githubusercontent.com/u/8558670?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/sreyemnayr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sreyemnayr "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>p3nj</sub>](https://benji.ltd/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=p3nj "Code") | [<img src="https://avatars.githubusercontent.com/u/6201617?v=4" width="110px;"/><br /><sub>Tim White</sub>](https://github.com/timwsuqld)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timwsuqld "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/southwolf"><img src="https://avatars0.githubusercontent.com/u/150648?v=3?s=110" width="110px;" alt="SouthWolf"/><br /><sub><b>SouthWolf</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=southwolf" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>yannikp</sub>](https://github.com/yannikp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=yannikp "Code") | [<img src="https://avatars.githubusercontent.com/u/20525448?v=4" width="110px;"/><br /><sub>victoria</sub>](https://github.com/viclou)<br />[💻](https://github.com/snipe/snipe-it/commits?author=viclou "Code") | [<img src="https://avatars.githubusercontent.com/u/40685314?v=4" width="110px;"/><br /><sub>Valentyn Tulub</sub>](https://github.com/valentyntu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=valentyntu "Code") | [<img src="https://avatars.githubusercontent.com/u/864520?v=4" width="110px;"/><br /><sub>Wouter van Os</sub>](http://wouter0100.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Wouter0100 "Code") | [<img src="https://avatars.githubusercontent.com/u/3946540?v=4" width="110px;"/><br /><sub>Wyatt Teeter</sub>](https://www.linkedin.com/in/wyatt-teeter)<br />[💻](https://github.com/snipe/snipe-it/commits?author=xWyatt "Code") | [<img src="https://avatars.githubusercontent.com/u/1596124?v=4" width="110px;"/><br /><sub>Yorick Terweijden</sub>](https://github.com/terwey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=terwey "Code") | [<img src="https://avatars.githubusercontent.com/u/69298836?v=4" width="110px;"/><br /><sub>bmkalle</sub>](https://github.com/bmkalle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bmkalle "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ivarne"><img src="https://avatars2.githubusercontent.com/u/131616?v=3?s=110" width="110px;" alt="Ivar Nesje"/><br /><sub><b>Ivar Nesje</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ivarne" title="Code">💻</a></td> | [<img src="https://avatars.githubusercontent.com/u/28403467?v=4" width="110px;"/><br /><sub>bricelabelle</sub>](https://github.com/bricelabelle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bricelabelle "Code") | [<img src="https://avatars.githubusercontent.com/u/97770090?v=4" width="110px;"/><br /><sub>corydlamb</sub>](https://github.com/corydlamb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=corydlamb "Code") | [<img src="https://avatars.githubusercontent.com/u/1154133?v=4" width="110px;"/><br /><sub>Diogenes S. Jesus</sub>](http://twitter.com/splash)<br />[💻](https://github.com/snipe/snipe-it/commits?author=splashx "Code") | [<img src="https://avatars.githubusercontent.com/u/5826629?v=4" width="110px;"/><br /><sub>D M</sub>](https://github.com/dkmansion)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkmansion "Code") | [<img src="https://avatars.githubusercontent.com/u/14837699?v=4" width="110px;"/><br /><sub>Dustin B</sub>](https://github.com/Jarli01)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jarli01 "Code") | [<img src="https://avatars.githubusercontent.com/u/348344?v=4" width="110px;"/><br /><sub>Fabian Grutschus</sub>](https://github.com/fabiang)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fabiang "Code") | [<img src="https://avatars.githubusercontent.com/u/1491053?v=4" width="110px;"/><br /><sub>MelonSmasher</sub>](https://github.com/MelonSmasher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MelonSmasher "Code") |
<td align="center" valign="top" width="14.28%"><a href="http://www.j0k3r.net"><img src="https://avatars1.githubusercontent.com/u/62333?v=3?s=110" width="110px;" alt="Jérémy Benoist"/><br /><sub><b>Jérémy Benoist</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=j0k3r" title="Documentation">📖</a></td> | [<img src="https://avatars.githubusercontent.com/u/80526133?v=4" width="110px;"/><br /><sub>AlexanderWPapyrus</sub>](https://github.com/AlexanderWPapyrus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AlexanderWPapyrus "Code") | [<img src="https://avatars.githubusercontent.com/u/306231?v=4" width="110px;"/><br /><sub>Alexandr Hacicheant</sub>](https://github.com/disc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=disc "Code") | [<img src="https://avatars.githubusercontent.com/u/3032891?v=4" width="110px;"/><br /><sub>Hex</sub>](https://hex128.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hex128 "Code") | [<img src="https://avatars.githubusercontent.com/u/8697942?v=4" width="110px;"/><br /><sub>Arunas Skirius</sub>](https://github.com/arukompas)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arukompas "Code") | [<img src="https://avatars.githubusercontent.com/u/104396?v=4" width="110px;"/><br /><sub>Ben Periton</sub>](https://github.com/benperiton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benperiton "Code") | [<img src="https://avatars.githubusercontent.com/u/11906832?v=4" width="110px;"/><br /><sub>Byron Wolfman</sub>](https://wolfman.dev/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=byronwolfman "Code") | [<img src="https://avatars.githubusercontent.com/u/56485508?v=4" width="110px;"/><br /><sub>Calvin</sub>](https://github.com/CalvinSchwartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CalvinSchwartz "Code") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cleathley"><img src="https://avatars2.githubusercontent.com/u/724344?v=3?s=110" width="110px;" alt="Chris Leathley"/><br /><sub><b>Chris Leathley</b></sub></a><br /><a href="#infra-cleathley" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> | [<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") |
<td align="center" valign="top" width="14.28%"><a href="https://github.com/splaer"><img src="https://avatars0.githubusercontent.com/u/972498?v=3?s=110" width="110px;" alt="splaer"/><br /><sub><b>splaer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Asplaer" title="Bug reports">🐛</a> <a href="https://github.com/snipe/snipe-it/commits?author=splaer" title="Code">💻</a></td> | [<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") |
</tr> | [<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") |
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.joeferguson.me"><img src="https://avatars1.githubusercontent.com/u/967362?v=3?s=110" width="110px;" alt="Joe Ferguson"/><br /><sub><b>Joe Ferguson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=svpernova09" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/diwanicki"><img src="https://avatars3.githubusercontent.com/u/6108682?v=3?s=110" width="110px;" alt="diwanicki"/><br /><sub><b>diwanicki</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=diwanicki" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=diwanicki" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pakkua80"><img src="https://avatars3.githubusercontent.com/u/2527115?v=3?s=110" width="110px;" alt="Lee Thoong Ching"/><br /><sub><b>Lee Thoong Ching</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=pakkua80" title="Documentation">📖</a> <a href="https://github.com/snipe/snipe-it/commits?author=pakkua80" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://shu.io"><img src="https://avatars1.githubusercontent.com/u/461491?v=3?s=110" width="110px;" alt="Marek Šuppa"/><br /><sub><b>Marek Šuppa</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mrshu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mizar1616"><img src="https://avatars1.githubusercontent.com/u/8693762?v=3?s=110" width="110px;" alt="Juan J. Martinez"/><br /><sub><b>Juan J. Martinez</b></sub></a><br /><a href="#translation-mizar1616" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rrdial"><img src="https://avatars1.githubusercontent.com/u/1458388?v=3?s=110" width="110px;" alt="R Ryan Dial"/><br /><sub><b>R Ryan Dial</b></sub></a><br /><a href="#translation-rrdial" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/burlito"><img src="https://avatars2.githubusercontent.com/u/2871745?v=3?s=110" width="110px;" alt="Andrej Manduch"/><br /><sub><b>Andrej Manduch</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=burlito" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.cordeos.com"><img src="https://avatars0.githubusercontent.com/u/8341172?v=3?s=110" width="110px;" alt="Jay Richards"/><br /><sub><b>Jay Richards</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=technogenus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://necurity.co.uk"><img src="https://avatars2.githubusercontent.com/u/7295127?v=3?s=110" width="110px;" alt="Alexander Innes"/><br /><sub><b>Alexander Innes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=leostat" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://buzzedword.codes"><img src="https://avatars2.githubusercontent.com/u/334485?v=3?s=110" width="110px;" alt="Danny Garcia"/><br /><sub><b>Danny Garcia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=buzzedword" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/archpoint"><img src="https://avatars2.githubusercontent.com/u/366855?v=3?s=110" width="110px;" alt="archpoint"/><br /><sub><b>archpoint</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=archpoint" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.jakemcgraw.com"><img src="https://avatars1.githubusercontent.com/u/67991?v=3?s=110" width="110px;" alt="Jake McGraw"/><br /><sub><b>Jake McGraw</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jakemcgraw" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FleischKarussel"><img src="https://avatars1.githubusercontent.com/u/1714374?v=3?s=110" width="110px;" alt="FleischKarussel"/><br /><sub><b>FleischKarussel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FleischKarussel" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/feeva"><img src="https://avatars3.githubusercontent.com/u/319644?v=3?s=110" width="110px;" alt="Dylan Yi"/><br /><sub><b>Dylan Yi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=feeva" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://FlashingCursor.com"><img src="https://avatars2.githubusercontent.com/u/857740?v=3?s=110" width="110px;" alt="Gil Rutkowski"/><br /><sub><b>Gil Rutkowski</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=flashingcursor" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.desmondmorris.com"><img src="https://avatars3.githubusercontent.com/u/129360?v=3?s=110" width="110px;" alt="Desmond Morris"/><br /><sub><b>Desmond Morris</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=desmondmorris" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://peelman.us"><img src="https://avatars2.githubusercontent.com/u/52936?v=3?s=110" width="110px;" alt="Nick Peelman"/><br /><sub><b>Nick Peelman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=peelman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://abrahamvegh.com"><img src="https://avatars0.githubusercontent.com/u/53161?v=3?s=110" width="110px;" alt="Abraham Vegh"/><br /><sub><b>Abraham Vegh</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=abrahamvegh" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rashivkp"><img src="https://avatars0.githubusercontent.com/u/2818680?v=3?s=110" width="110px;" alt="Mohamed Rashid"/><br /><sub><b>Mohamed Rashid</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rashivkp" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hinchk.github.io"><img src="https://avatars3.githubusercontent.com/u/1509456?v=3?s=110" width="110px;" alt="Kasey"/><br /><sub><b>Kasey</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=HinchK" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BrettFagerlund"><img src="https://avatars2.githubusercontent.com/u/10522541?v=3?s=110" width="110px;" alt="Brett"/><br /><sub><b>Brett</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BrettFagerlund" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://jasonspriggs.com"><img src="https://avatars2.githubusercontent.com/u/16108587?v=3?s=110" width="110px;" alt="Jason Spriggs"/><br /><sub><b>Jason Spriggs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jasonspriggs" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://n8felton.wordpress.com"><img src="https://avatars2.githubusercontent.com/u/1134568?v=3?s=110" width="110px;" alt="Nate Felton"/><br /><sub><b>Nate Felton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=n8felton" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://homepages.dcc.ufmg.br/~manassesferreira"><img src="https://avatars2.githubusercontent.com/u/14036694?v=3?s=110" width="110px;" alt="Manasses Ferreira"/><br /><sub><b>Manasses Ferreira</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=manassesferreira" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/steveelwood"><img src="https://avatars0.githubusercontent.com/u/15913949?v=3?s=110" width="110px;" alt="Steve"/><br /><sub><b>Steve</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=steveelwood" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/matc"><img src="https://avatars1.githubusercontent.com/u/3361683?v=3?s=110" width="110px;" alt="matc"/><br /><sub><b>matc</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=matc" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.davisracingteam.com"><img src="https://avatars3.githubusercontent.com/u/7405702?v=3?s=110" width="110px;" alt="Cole R. Davis"/><br /><sub><b>Cole R. Davis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gibsonjoshua55"><img src="https://avatars2.githubusercontent.com/u/10167681?v=3?s=110" width="110px;" alt="gibsonjoshua55"/><br /><sub><b>gibsonjoshua55</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zwerch"><img src="https://avatars2.githubusercontent.com/u/2809241?v=4?s=110" width="110px;" alt="Robin Temme"/><br /><sub><b>Robin Temme</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zwerch" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imanghafoori1"><img src="https://avatars0.githubusercontent.com/u/6961695?v=4?s=110" width="110px;" alt="Iman"/><br /><sub><b>Iman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=imanghafoori1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richardhofman6"><img src="https://avatars1.githubusercontent.com/u/6551003?v=4?s=110" width="110px;" alt="Richard Hofman"/><br /><sub><b>Richard Hofman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=richardhofman6" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gizzmojr"><img src="https://avatars0.githubusercontent.com/u/3697569?v=4?s=110" width="110px;" alt="gizzmojr"/><br /><sub><b>gizzmojr</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gizzmojr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imjennyli"><img src="https://avatars3.githubusercontent.com/u/404729?v=4?s=110" width="110px;" alt="Jenny Li"/><br /><sub><b>Jenny Li</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=imjennyli" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GeoffYoung"><img src="https://avatars0.githubusercontent.com/u/869227?v=4?s=110" width="110px;" alt="Geoff Young"/><br /><sub><b>Geoff Young</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=GeoffYoung" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.elliotblackburn.com"><img src="https://avatars3.githubusercontent.com/u/1068477?v=4?s=110" width="110px;" alt="Elliot Blackburn"/><br /><sub><b>Elliot Blackburn</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BlueHatbRit" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://andmemasin.eu"><img src="https://avatars1.githubusercontent.com/u/6357451?v=4?s=110" width="110px;" alt="Tõnis Ormisson"/><br /><sub><b>Tõnis Ormisson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TonisOrmisson" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.nicolai-essig.de"><img src="https://avatars0.githubusercontent.com/u/449411?v=4?s=110" width="110px;" alt="Nicolai Essig"/><br /><sub><b>Nicolai Essig</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thakilla" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/techincolor"><img src="https://avatars1.githubusercontent.com/u/14809698?v=4?s=110" width="110px;" alt="Danielle"/><br /><sub><b>Danielle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=techincolor" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TheVakman"><img src="https://avatars1.githubusercontent.com/u/18545156?v=4?s=110" width="110px;" alt="Lawrence"/><br /><sub><b>Lawrence</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TheVakman" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/uknzaeinozpas"><img src="https://avatars1.githubusercontent.com/u/22473767?v=4?s=110" width="110px;" alt="uknzaeinozpas"/><br /><sub><b>uknzaeinozpas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Gelob"><img src="https://avatars3.githubusercontent.com/u/422752?v=4?s=110" width="110px;" alt="Ryan"/><br /><sub><b>Ryan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Gelob" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vcordes79"><img src="https://avatars1.githubusercontent.com/u/10672546?v=4?s=110" width="110px;" alt="vcordes79"/><br /><sub><b>vcordes79</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vcordes79" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fordster78"><img src="https://avatars3.githubusercontent.com/u/27958330?v=4?s=110" width="110px;" alt="fordster78"/><br /><sub><b>fordster78</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fordster78" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CronKz"><img src="https://avatars0.githubusercontent.com/u/34064225?v=4?s=110" width="110px;" alt="CronKz"/><br /><sub><b>CronKz</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=CronKz" title="Code">💻</a> <a href="#translation-CronKz" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tdb"><img src="https://avatars1.githubusercontent.com/u/585486?v=4?s=110" width="110px;" alt="Tim Bishop"/><br /><sub><b>Tim Bishop</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tdb" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.seanmcilvenna.com"><img src="https://avatars2.githubusercontent.com/u/5384694?v=4?s=110" width="110px;" alt="Sean McIlvenna"/><br /><sub><b>Sean McIlvenna</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=seanmcilvenna" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cepacs"><img src="https://avatars3.githubusercontent.com/u/36515590?v=4?s=110" width="110px;" alt="cepacs"/><br /><sub><b>cepacs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Acepacs" title="Bug reports">🐛</a> <a href="https://github.com/snipe/snipe-it/commits?author=cepacs" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lea-mink"><img src="https://avatars2.githubusercontent.com/u/37537300?v=4?s=110" width="110px;" alt="lea-mink"/><br /><sub><b>lea-mink</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lea-mink" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hannahtinkler"><img src="https://avatars0.githubusercontent.com/u/7140719?v=4?s=110" width="110px;" alt="Hannah Tinkler"/><br /><sub><b>Hannah Tinkler</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hannahtinkler" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doekman"><img src="https://avatars1.githubusercontent.com/u/1086388?v=4?s=110" width="110px;" alt="Doeke Zanstra"/><br /><sub><b>Doeke Zanstra</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=doekman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.sdhd.nl/"><img src="https://avatars1.githubusercontent.com/u/4325936?v=4?s=110" width="110px;" alt="Djamon Staal"/><br /><sub><b>Djamon Staal</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=SjamonDaal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EarlRamirez"><img src="https://avatars3.githubusercontent.com/u/12306859?v=4?s=110" width="110px;" alt="Earl Ramirez"/><br /><sub><b>Earl Ramirez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=EarlRamirez" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RichardRay"><img src="https://avatars2.githubusercontent.com/u/8671456?v=4?s=110" width="110px;" alt="Richard Ray Thomas"/><br /><sub><b>Richard Ray Thomas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=RichardRay" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=110" width="110px;" alt="Ryan Kuba"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thelamer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ParadoxGuitarist"><img src="https://avatars1.githubusercontent.com/u/6751928?v=4?s=110" width="110px;" alt="Brian Monroe"/><br /><sub><b>Brian Monroe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/plexorama"><img src="https://avatars1.githubusercontent.com/u/605167?v=4?s=110" width="110px;" alt="plexorama"/><br /><sub><b>plexorama</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=plexorama" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://tilldeeke.de"><img src="https://avatars2.githubusercontent.com/u/1795149?v=4?s=110" width="110px;" alt="Till Deeke"/><br /><sub><b>Till Deeke</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tilldeeke" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/5quirrel"><img src="https://avatars0.githubusercontent.com/u/12634129?v=4?s=110" width="110px;" alt="5quirrel"/><br /><sub><b>5quirrel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=5quirrel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jasonlshelton"><img src="https://avatars1.githubusercontent.com/u/13071957?v=4?s=110" width="110px;" alt="Jason"/><br /><sub><b>Jason</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jasonlshelton" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chemfy"><img src="https://avatars3.githubusercontent.com/u/7128321?v=4?s=110" width="110px;" alt="Antti"/><br /><sub><b>Antti</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chemfy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DeusMaximus"><img src="https://avatars3.githubusercontent.com/u/10080364?v=4?s=110" width="110px;" alt="DeusMaximus"/><br /><sub><b>DeusMaximus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DeusMaximus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/A-ROYAL"><img src="https://avatars2.githubusercontent.com/u/16384611?v=4?s=110" width="110px;" alt="a-royal"/><br /><sub><b>a-royal</b></sub></a><br /><a href="#translation-A-ROYAL" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/albertoaldrigo"><img src="https://avatars0.githubusercontent.com/u/5358208?v=4?s=110" width="110px;" alt="Alberto Aldrigo"/><br /><sub><b>Alberto Aldrigo</b></sub></a><br /><a href="#translation-albertoaldrigo" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://alex.stanev.org/blog"><img src="https://avatars0.githubusercontent.com/u/1412342?v=4?s=110" width="110px;" alt="Alex Stanev"/><br /><sub><b>Alex Stanev</b></sub></a><br /><a href="#translation-RealEnder" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://devel.itsolution2.de"><img src="https://avatars0.githubusercontent.com/u/177295?v=4?s=110" width="110px;" alt="Andreas Rehm"/><br /><sub><b>Andreas Rehm</b></sub></a><br /><a href="#translation-sirrus" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xelan"><img src="https://avatars0.githubusercontent.com/u/5080535?v=4?s=110" width="110px;" alt="Andreas Erhard"/><br /><sub><b>Andreas Erhard</b></sub></a><br /><a href="#translation-xelan" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/angeldeejay"><img src="https://avatars2.githubusercontent.com/u/142350?v=4?s=110" width="110px;" alt="Andrés Vanegas Jiménez"/><br /><sub><b>Andrés Vanegas Jiménez</b></sub></a><br /><a href="#translation-angeldeejay" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aschiavon91"><img src="https://avatars0.githubusercontent.com/u/3910403?v=4?s=110" width="110px;" alt="Antonio Schiavon"/><br /><sub><b>Antonio Schiavon</b></sub></a><br /><a href="#translation-aschiavon91" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benunter"><img src="https://avatars0.githubusercontent.com/u/10464547?v=4?s=110" width="110px;" alt="benunter"/><br /><sub><b>benunter</b></sub></a><br /><a href="#translation-benunter" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://catweb24.pl"><img src="https://avatars1.githubusercontent.com/u/5038647?v=4?s=110" width="110px;" alt="Borys Żmuda"/><br /><sub><b>Borys Żmuda</b></sub></a><br /><a href="#translation-rudashi" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chibacityblues"><img src="https://avatars0.githubusercontent.com/u/5539359?v=4?s=110" width="110px;" alt="chibacityblues"/><br /><sub><b>chibacityblues</b></sub></a><br /><a href="#translation-chibacityblues" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cwlin0416"><img src="https://avatars1.githubusercontent.com/u/1954830?v=4?s=110" width="110px;" alt="Chien Wei Lin"/><br /><sub><b>Chien Wei Lin</b></sub></a><br /><a href="#translation-cwlin0416" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Againstreality"><img src="https://avatars3.githubusercontent.com/u/11700533?v=4?s=110" width="110px;" alt="Christian Schuster"/><br /><sub><b>Christian Schuster</b></sub></a><br /><a href="#translation-Againstreality" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://chriss.webhostid.com"><img src="https://avatars1.githubusercontent.com/u/4308704?v=4?s=110" width="110px;" alt="Christian Stefanus"/><br /><sub><b>Christian Stefanus</b></sub></a><br /><a href="#translation-kopi-item" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://wxcafe.net"><img src="https://avatars3.githubusercontent.com/u/3009327?v=4?s=110" width="110px;" alt="wxcafé"/><br /><sub><b>wxcafé</b></sub></a><br /><a href="#translation-wxcafe" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dpyroc"><img src="https://avatars3.githubusercontent.com/u/35761525?v=4?s=110" width="110px;" alt="dpyroc"/><br /><sub><b>dpyroc</b></sub></a><br /><a href="#translation-dpyroc" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.friedlmaier.net"><img src="https://avatars1.githubusercontent.com/u/2153639?v=4?s=110" width="110px;" alt="Daniel Friedlmaier"/><br /><sub><b>Daniel Friedlmaier</b></sub></a><br /><a href="#translation-da-friedl" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielheene"><img src="https://avatars1.githubusercontent.com/u/2947640?v=4?s=110" width="110px;" alt="Daniel Heene"/><br /><sub><b>Daniel Heene</b></sub></a><br /><a href="#translation-danielheene" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielcb"><img src="https://avatars3.githubusercontent.com/u/319022?v=4?s=110" width="110px;" alt="danielcb"/><br /><sub><b>danielcb</b></sub></a><br /><a href="#translation-danielcb" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dominiksenti"><img src="https://avatars3.githubusercontent.com/u/15846537?v=4?s=110" width="110px;" alt="Dominik Senti"/><br /><sub><b>Dominik Senti</b></sub></a><br /><a href="#translation-dominiksenti" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.konectik.com"><img src="https://avatars0.githubusercontent.com/u/25570954?v=4?s=110" width="110px;" alt="Eric Gautheron"/><br /><sub><b>Eric Gautheron</b></sub></a><br /><a href="#translation-EpixFr" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://erlpil.com"><img src="https://avatars1.githubusercontent.com/u/5732623?v=4?s=110" width="110px;" alt="Erlend Pilø"/><br /><sub><b>Erlend Pilø</b></sub></a><br /><a href="#translation-Erlpil" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://fabio.technology"><img src="https://avatars0.githubusercontent.com/u/541832?v=4?s=110" width="110px;" alt="Fabio Rapposelli"/><br /><sub><b>Fabio Rapposelli</b></sub></a><br /><a href="#translation-frapposelli" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fgbs"><img src="https://avatars2.githubusercontent.com/u/3605240?v=4?s=110" width="110px;" alt="Felipe Barros"/><br /><sub><b>Felipe Barros</b></sub></a><br /><a href="#translation-fgbs" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/possebon"><img src="https://avatars0.githubusercontent.com/u/257745?v=4?s=110" width="110px;" alt="Fernando Possebon"/><br /><sub><b>Fernando Possebon</b></sub></a><br /><a href="#translation-possebon" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gdraque"><img src="https://avatars3.githubusercontent.com/u/2540832?v=4?s=110" width="110px;" alt="gdraque"/><br /><sub><b>gdraque</b></sub></a><br /><a href="#translation-gdraque" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/georgwallisch"><img src="https://avatars0.githubusercontent.com/u/23440381?v=4?s=110" width="110px;" alt="Georg Wallisch"/><br /><sub><b>Georg Wallisch</b></sub></a><br /><a href="#translation-georgwallisch" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jgroblesr85"><img src="https://avatars1.githubusercontent.com/u/9852832?v=4?s=110" width="110px;" alt="Gerardo Robles"/><br /><sub><b>Gerardo Robles</b></sub></a><br /><a href="#translation-jgroblesr85" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://t.me/Gluek"><img src="https://avatars2.githubusercontent.com/u/11082640?v=4?s=110" width="110px;" alt="Gluek"/><br /><sub><b>Gluek</b></sub></a><br /><a href="#translation-mrgluek" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AdnanAbuShahad"><img src="https://avatars0.githubusercontent.com/u/6847946?v=4?s=110" width="110px;" alt="AdnanAbuShahad"/><br /><sub><b>AdnanAbuShahad</b></sub></a><br /><a href="#translation-AdnanAbuShahad" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://hafidzi.my"><img src="https://avatars1.githubusercontent.com/u/3580608?v=4?s=110" width="110px;" alt="Hafidzi My"/><br /><sub><b>Hafidzi My</b></sub></a><br /><a href="#translation-hafidzi" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fofwisdom"><img src="https://avatars2.githubusercontent.com/u/205521?v=4?s=110" width="110px;" alt="Harim Park"/><br /><sub><b>Harim Park</b></sub></a><br /><a href="#translation-fofwisdom" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.kentsson.se"><img src="https://avatars2.githubusercontent.com/u/3333841?v=4?s=110" width="110px;" alt="Henrik Kentsson"/><br /><sub><b>Henrik Kentsson</b></sub></a><br /><a href="#translation-Kentsson" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/husnulyaqien"><img src="https://avatars0.githubusercontent.com/u/36551034?v=4?s=110" width="110px;" alt="Husnul Yaqien"/><br /><sub><b>Husnul Yaqien</b></sub></a><br /><a href="#translation-husnulyaqien" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://abaalkhail.org"><img src="https://avatars1.githubusercontent.com/u/2372747?v=4?s=110" width="110px;" alt="Ibrahim"/><br /><sub><b>Ibrahim</b></sub></a><br /><a href="#translation-abaalkh" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/igolman"><img src="https://avatars0.githubusercontent.com/u/1389334?v=4?s=110" width="110px;" alt="igolman"/><br /><sub><b>igolman</b></sub></a><br /><a href="#translation-igolman" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itangiang"><img src="https://avatars1.githubusercontent.com/u/3257070?v=4?s=110" width="110px;" alt="itangiang"/><br /><sub><b>itangiang</b></sub></a><br /><a href="#translation-itangiang" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jarby1211"><img src="https://avatars2.githubusercontent.com/u/14814254?v=4?s=110" width="110px;" alt="jarby1211"/><br /><sub><b>jarby1211</b></sub></a><br /><a href="#translation-jarby1211" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://jwillker.com"><img src="https://avatars3.githubusercontent.com/u/6719357?v=4?s=110" width="110px;" alt="Jhonn Willker"/><br /><sub><b>Jhonn Willker</b></sub></a><br /><a href="#translation-JohnWillker" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joxelito94"><img src="https://avatars2.githubusercontent.com/u/10983635?v=4?s=110" width="110px;" alt="Jose"/><br /><sub><b>Jose</b></sub></a><br /><a href="#translation-joxelito94" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/laopangzi"><img src="https://avatars0.githubusercontent.com/u/5206122?v=4?s=110" width="110px;" alt="laopangzi"/><br /><sub><b>laopangzi</b></sub></a><br /><a href="#translation-laopangzi" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://usrportage.de"><img src="https://avatars2.githubusercontent.com/u/79707?v=4?s=110" width="110px;" alt="Lars Strojny"/><br /><sub><b>Lars Strojny</b></sub></a><br /><a href="#translation-lstrojny" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/marcosbl"><img src="https://avatars0.githubusercontent.com/u/389801?v=4?s=110" width="110px;" alt="MarcosBL"/><br /><sub><b>MarcosBL</b></sub></a><br /><a href="#translation-MarcosBL" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mariejoyacajes"><img src="https://avatars3.githubusercontent.com/u/35664606?v=4?s=110" width="110px;" alt="marie joy cajes"/><br /><sub><b>marie joy cajes</b></sub></a><br /><a href="#translation-mariejoyacajes" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.markjohansen.dk"><img src="https://avatars2.githubusercontent.com/u/3052816?v=4?s=110" width="110px;" alt="Mark S. Johansen"/><br /><sub><b>Mark S. Johansen</b></sub></a><br /><a href="#translation-msjohansen" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://martinstub.dk"><img src="https://avatars2.githubusercontent.com/u/982885?v=4?s=110" width="110px;" alt="Martin Stub"/><br /><sub><b>Martin Stub</b></sub></a><br /><a href="#translation-stubben" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/meyerf99"><img src="https://avatars2.githubusercontent.com/u/28959963?v=4?s=110" width="110px;" alt="Meyer Flavio"/><br /><sub><b>Meyer Flavio</b></sub></a><br /><a href="#translation-meyerf99" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MicaelRodrigues"><img src="https://avatars3.githubusercontent.com/u/796443?v=4?s=110" width="110px;" alt="Micael Rodrigues"/><br /><sub><b>Micael Rodrigues</b></sub></a><br /><a href="#translation-MicaelRodrigues" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://rubixy.com/"><img src="https://avatars0.githubusercontent.com/u/10481331?v=4?s=110" width="110px;" alt="Mikael Rasmussen"/><br /><sub><b>Mikael Rasmussen</b></sub></a><br /><a href="#translation-mikaelssen" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IxFail"><img src="https://avatars1.githubusercontent.com/u/1544552?v=4?s=110" width="110px;" alt="IxFail"/><br /><sub><b>IxFail</b></sub></a><br /><a href="#translation-IxFail" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.mohammedfota.com"><img src="https://avatars3.githubusercontent.com/u/18483118?v=4?s=110" width="110px;" alt="Mohammed Fota"/><br /><sub><b>Mohammed Fota</b></sub></a><br /><a href="#translation-MohammedFota" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/omego"><img src="https://avatars0.githubusercontent.com/u/227080?v=4?s=110" width="110px;" alt="Moayad Alserihi"/><br /><sub><b>Moayad Alserihi</b></sub></a><br /><a href="#translation-omego" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/saymd"><img src="https://avatars0.githubusercontent.com/u/1680266?v=4?s=110" width="110px;" alt="saymd"/><br /><sub><b>saymd</b></sub></a><br /><a href="#translation-saymd" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nordsken.se"><img src="https://avatars0.githubusercontent.com/u/1826808?v=4?s=110" width="110px;" alt="Patrik Larsson"/><br /><sub><b>Patrik Larsson</b></sub></a><br /><a href="#translation-pooot" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drcryo"><img src="https://avatars1.githubusercontent.com/u/20584746?v=4?s=110" width="110px;" alt="drcryo"/><br /><sub><b>drcryo</b></sub></a><br /><a href="#translation-drcryo" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pawel1615"><img src="https://avatars1.githubusercontent.com/u/19408004?v=4?s=110" width="110px;" alt="pawel1615"/><br /><sub><b>pawel1615</b></sub></a><br /><a href="#translation-pawel1615" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bodrovics"><img src="https://avatars2.githubusercontent.com/u/23340468?v=4?s=110" width="110px;" alt="bodrovics"/><br /><sub><b>bodrovics</b></sub></a><br /><a href="#translation-bodrovics" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/priatna"><img src="https://avatars0.githubusercontent.com/u/3257654?v=4?s=110" width="110px;" alt="priatna"/><br /><sub><b>priatna</b></sub></a><br /><a href="#translation-priatna" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://amayume.net"><img src="https://avatars1.githubusercontent.com/u/5358374?v=4?s=110" width="110px;" alt="Fan Jiang"/><br /><sub><b>Fan Jiang</b></sub></a><br /><a href="#translation-ProfFan" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ragnarcx"><img src="https://avatars1.githubusercontent.com/u/22555451?v=4?s=110" width="110px;" alt="ragnarcx"/><br /><sub><b>ragnarcx</b></sub></a><br /><a href="#translation-ragnarcx" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.reinvanhaaren.nl/"><img src="https://avatars2.githubusercontent.com/u/18654582?v=4?s=110" width="110px;" alt="Rein van Haaren"/><br /><sub><b>Rein van Haaren</b></sub></a><br /><a href="#translation-reinvanhaaren" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://dheche.songolimo.net"><img src="https://avatars1.githubusercontent.com/u/386672?v=4?s=110" width="110px;" alt="Teguh Dwicaksana"/><br /><sub><b>Teguh Dwicaksana</b></sub></a><br /><a href="#translation-dheche" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FRaccie"><img src="https://avatars2.githubusercontent.com/u/2572552?v=4?s=110" width="110px;" alt="fraccie"/><br /><sub><b>fraccie</b></sub></a><br /><a href="#translation-FRaccie" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinzruzell"><img src="https://avatars0.githubusercontent.com/u/35182720?v=4?s=110" width="110px;" alt="vinzruzell"/><br /><sub><b>vinzruzell</b></sub></a><br /><a href="#translation-vinzruzell" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://kevinaustin.com"><img src="https://avatars1.githubusercontent.com/u/7883603?v=4?s=110" width="110px;" alt="Kevin Austin"/><br /><sub><b>Kevin Austin</b></sub></a><br /><a href="#translation-vipsystem" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://azuraweb.xyz"><img src="https://avatars3.githubusercontent.com/u/3861828?v=4?s=110" width="110px;" alt="Wira Sandy"/><br /><sub><b>Wira Sandy</b></sub></a><br /><a href="#translation-wira-sandy" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GrayHoax"><img src="https://avatars2.githubusercontent.com/u/8663789?v=4?s=110" width="110px;" alt="Илья"/><br /><sub><b>Илья</b></sub></a><br /><a href="#translation-GrayHoax" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/godusevpn"><img src="https://avatars3.githubusercontent.com/u/30119111?v=4?s=110" width="110px;" alt="GodUseVPN"/><br /><sub><b>GodUseVPN</b></sub></a><br /><a href="#translation-godusevpn" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EngrZhou"><img src="https://avatars1.githubusercontent.com/u/745576?v=4?s=110" width="110px;" alt="周周"/><br /><sub><b>周周</b></sub></a><br /><a href="#translation-EngrZhou" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/takuy"><img src="https://avatars3.githubusercontent.com/u/1631095?v=4?s=110" width="110px;" alt="Sam"/><br /><sub><b>Sam</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=takuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.illisian.com.au"><img src="https://avatars1.githubusercontent.com/u/264022?v=4?s=110" width="110px;" alt="Azerothian"/><br /><sub><b>Azerothian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Azerothian" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://macfoo.wordpress.com/"><img src="https://avatars1.githubusercontent.com/u/4930051?v=4?s=110" width="110px;" alt="Wes Hulette"/><br /><sub><b>Wes Hulette</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jwhulette" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/patrict"><img src="https://avatars0.githubusercontent.com/u/8134591?v=4?s=110" width="110px;" alt="patrict"/><br /><sub><b>patrict</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=patrict" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VELIKII-DIVAN"><img src="https://avatars3.githubusercontent.com/u/2611616?v=4?s=110" width="110px;" alt="Dmitriy Minaev"/><br /><sub><b>Dmitriy Minaev</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liquidhorse"><img src="https://avatars0.githubusercontent.com/u/5132245?v=4?s=110" width="110px;" alt="liquidhorse"/><br /><sub><b>liquidhorse</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=liquidhorse" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://seld.be/"><img src="https://avatars1.githubusercontent.com/u/183678?v=4?s=110" width="110px;" alt="Jordi Boggiano"/><br /><sub><b>Jordi Boggiano</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Seldaek" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/inietov"><img src="https://avatars0.githubusercontent.com/u/653557?v=4?s=110" width="110px;" alt="Ivan Nieto"/><br /><sub><b>Ivan Nieto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=inietov" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benrubson"><img src="https://avatars2.githubusercontent.com/u/6764151?v=4?s=110" width="110px;" alt="Ben RUBSON"/><br /><sub><b>Ben RUBSON</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benrubson" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NMathar"><img src="https://avatars2.githubusercontent.com/u/8554558?v=4?s=110" width="110px;" alt="NMathar"/><br /><sub><b>NMathar</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NMathar" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/smb"><img src="https://avatars1.githubusercontent.com/u/139566?v=4?s=110" width="110px;" alt="Steffen"/><br /><sub><b>Steffen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=smb" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Sxderp"><img src="https://avatars0.githubusercontent.com/u/6609453?v=4?s=110" width="110px;" alt="Sxderp"/><br /><sub><b>Sxderp</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Sxderp" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fanta8897"><img src="https://avatars1.githubusercontent.com/u/4807843?v=4?s=110" width="110px;" alt="fanta8897"/><br /><sub><b>fanta8897</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fanta8897" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://andreybolonin.com/phpconsulting/"><img src="https://avatars2.githubusercontent.com/u/2576509?v=4?s=110" width="110px;" alt="Andrey Bolonin"/><br /><sub><b>Andrey Bolonin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andreybolonin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.shinayoshi.net/"><img src="https://avatars3.githubusercontent.com/u/2173307?v=4?s=110" width="110px;" alt="shinayoshi"/><br /><sub><b>shinayoshi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=shinayoshi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reuser"><img src="https://avatars3.githubusercontent.com/u/2130159?v=4?s=110" width="110px;" alt="Hubert"/><br /><sub><b>Hubert</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=reuser" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://brashear.me"><img src="https://avatars0.githubusercontent.com/u/6865789?v=4?s=110" width="110px;" alt="KeenRivals"/><br /><sub><b>KeenRivals</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=KeenRivals" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/omyno"><img src="https://avatars3.githubusercontent.com/u/2902513?v=4?s=110" width="110px;" alt="omyno"/><br /><sub><b>omyno</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=omyno" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jackka"><img src="https://avatars1.githubusercontent.com/u/6271335?v=4?s=110" width="110px;" alt="Evgeny"/><br /><sub><b>Evgeny</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jackka" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://digitalist.se"><img src="https://avatars2.githubusercontent.com/u/1169963?v=4?s=110" width="110px;" alt="Colin Campbell"/><br /><sub><b>Colin Campbell</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=colin-campbell" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lubo"><img src="https://avatars3.githubusercontent.com/u/2872098?v=4?s=110" width="110px;" alt="Ľubomír Kučera"/><br /><sub><b>Ľubomír Kučera</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lubo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.sourceguru.net"><img src="https://avatars3.githubusercontent.com/u/570639?v=4?s=110" width="110px;" alt="Martin Meredith"/><br /><sub><b>Martin Meredith</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Mezzle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/timothyfarmer"><img src="https://avatars1.githubusercontent.com/u/7632599?v=4?s=110" width="110px;" alt="Tim Farmer"/><br /><sub><b>Tim Farmer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=timothyfarmer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mskrip"><img src="https://avatars0.githubusercontent.com/u/17459600?v=4?s=110" width="110px;" alt="Marián Skrip"/><br /><sub><b>Marián Skrip</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mskrip" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Godmartinz"><img src="https://avatars2.githubusercontent.com/u/47435081?v=4?s=110" width="110px;" alt="Godfrey Martinez"/><br /><sub><b>Godfrey Martinez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Godmartinz" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bigtreeEdo"><img src="https://avatars1.githubusercontent.com/u/2075128?v=4?s=110" width="110px;" alt="bigtreeEdo"/><br /><sub><b>bigtreeEdo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bigtreeEdo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://colinmcneil.me/"><img src="https://avatars0.githubusercontent.com/u/5000430?v=4?s=110" width="110px;" alt="Colin McNeil"/><br /><sub><b>Colin McNeil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ColinMcNeil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JoKneeMo"><img src="https://avatars0.githubusercontent.com/u/421625?v=4?s=110" width="110px;" alt="JoKneeMo"/><br /><sub><b>JoKneeMo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JoKneeMo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.redbridge.se"><img src="https://avatars0.githubusercontent.com/u/54849013?v=4?s=110" width="110px;" alt="Joshi"/><br /><sub><b>Joshi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=joshi-redbridge" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anthonypburns"><img src="https://avatars2.githubusercontent.com/u/15731458?v=4?s=110" width="110px;" alt="Anthony Burns"/><br /><sub><b>Anthony Burns</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=anthonypburns" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/johnson-yi"><img src="https://avatars1.githubusercontent.com/u/63399474?v=4?s=110" width="110px;" alt="johnson-yi"/><br /><sub><b>johnson-yi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=johnson-yi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://tangentmc.net"><img src="https://avatars1.githubusercontent.com/u/1862720?v=4?s=110" width="110px;" alt="Sanjay Govind"/><br /><sub><b>Sanjay Govind</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sanjay900" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://peter.upfold.org.uk/"><img src="https://avatars0.githubusercontent.com/u/1255375?v=4?s=110" width="110px;" alt="Peter Upfold"/><br /><sub><b>Peter Upfold</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PeterUpfold" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jbiel"><img src="https://avatars2.githubusercontent.com/u/961717?v=4?s=110" width="110px;" alt="Jared Biel"/><br /><sub><b>Jared Biel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jbiel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dampfklon"><img src="https://avatars1.githubusercontent.com/u/1733625?v=4?s=110" width="110px;" alt="Dampfklon"/><br /><sub><b>Dampfklon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dampfklon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://communityclosing.com"><img src="https://avatars2.githubusercontent.com/u/52973156?v=4?s=110" width="110px;" alt="Charles Hamilton"/><br /><sub><b>Charles Hamilton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chamilton-ccn" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/giannello"><img src="https://avatars.githubusercontent.com/u/551789?v=4?s=110" width="110px;" alt="Giuseppe Iannello"/><br /><sub><b>Giuseppe Iannello</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=giannello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.peterdavehello.org/"><img src="https://avatars.githubusercontent.com/u/3691490?v=4?s=110" width="110px;" alt="Peter Dave Hello"/><br /><sub><b>Peter Dave Hello</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PeterDaveHello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sigmoidal"><img src="https://avatars.githubusercontent.com/u/6106332?v=4?s=110" width="110px;" alt="sigmoidal"/><br /><sub><b>sigmoidal</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sigmoidal" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/phenixdotnet"><img src="https://avatars.githubusercontent.com/u/2082554?v=4?s=110" width="110px;" alt="Vincent Lainé"/><br /><sub><b>Vincent Lainé</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=phenixdotnet" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.lucas-pless.com"><img src="https://avatars.githubusercontent.com/u/1943040?v=4?s=110" width="110px;" alt="Lucas Pleß"/><br /><sub><b>Lucas Pleß</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=derlucas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/iansltx"><img src="https://avatars.githubusercontent.com/u/472804?v=4?s=110" width="110px;" alt="Ian Littman"/><br /><sub><b>Ian Littman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=iansltx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PauloLuna"><img src="https://avatars.githubusercontent.com/u/3519029?v=4?s=110" width="110px;" alt="João Paulo"/><br /><sub><b>João Paulo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PauloLuna" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThoBur"><img src="https://avatars.githubusercontent.com/u/70443365?v=4?s=110" width="110px;" alt="ThoBur"/><br /><sub><b>ThoBur</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ThoBur" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://phpprofi.ru/"><img src="https://avatars.githubusercontent.com/u/1972329?v=4?s=110" width="110px;" alt="Alexander Chibrikin"/><br /><sub><b>Alexander Chibrikin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=alek13" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winstan"><img src="https://avatars.githubusercontent.com/u/438332?v=4?s=110" width="110px;" alt="Anthony Winstanley"/><br /><sub><b>Anthony Winstanley</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=winstan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fashberg"><img src="https://avatars.githubusercontent.com/u/3075214?v=4?s=110" width="110px;" alt="Folke"/><br /><sub><b>Folke</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fashberg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benwa"><img src="https://avatars.githubusercontent.com/u/1351571?v=4?s=110" width="110px;" alt="Bennett Blodinger"/><br /><sub><b>Bennett Blodinger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benwa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nmc.dev"><img src="https://avatars.githubusercontent.com/u/2974631?v=4?s=110" width="110px;" alt="NMC"/><br /><sub><b>NMC</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ncareau" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andres-baller"><img src="https://avatars.githubusercontent.com/u/52182449?v=4?s=110" width="110px;" alt="andres-baller"/><br /><sub><b>andres-baller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andres-baller" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sean-borg"><img src="https://avatars.githubusercontent.com/u/67109348?v=4?s=110" width="110px;" alt="sean-borg"/><br /><sub><b>sean-borg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sean-borg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EDVLeer"><img src="https://avatars.githubusercontent.com/u/32170051?v=4?s=110" width="110px;" alt="EDVLeer"/><br /><sub><b>EDVLeer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=EDVLeer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kurokat"><img src="https://avatars.githubusercontent.com/u/23075196?v=4?s=110" width="110px;" alt="Kurokat"/><br /><sub><b>Kurokat</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Kurokat" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.kevinkoellmann.de"><img src="https://avatars.githubusercontent.com/u/915514?v=4?s=110" width="110px;" alt="Kevin Köllmann"/><br /><sub><b>Kevin Köllmann</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=koelle25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sw-mreyes"><img src="https://avatars.githubusercontent.com/u/49025941?v=4?s=110" width="110px;" alt="sw-mreyes"/><br /><sub><b>sw-mreyes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sw-mreyes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://pittet.ca"><img src="https://avatars.githubusercontent.com/u/70129?v=4?s=110" width="110px;" alt="Joel Pittet"/><br /><sub><b>Joel Pittet</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=joelpittet" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://elyscape.com"><img src="https://avatars.githubusercontent.com/u/792695?v=4?s=110" width="110px;" alt="Eli Young"/><br /><sub><b>Eli Young</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=elyscape" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/raelldottin"><img src="https://avatars.githubusercontent.com/u/317015?v=4?s=110" width="110px;" alt="Raell Dottin"/><br /><sub><b>Raell Dottin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=raelldottin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/misilot"><img src="https://avatars.githubusercontent.com/u/1446856?v=4?s=110" width="110px;" alt="Tom Misilo"/><br /><sub><b>Tom Misilo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=misilot" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://david.davenne.be"><img src="https://avatars.githubusercontent.com/u/4496300?v=4?s=110" width="110px;" alt="David Davenne"/><br /><sub><b>David Davenne</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JuustoMestari" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://markstenglein.com"><img src="https://avatars.githubusercontent.com/u/9255772?v=4?s=110" width="110px;" alt="Mark Stenglein"/><br /><sub><b>Mark Stenglein</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ocelotsloth" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ajsy"><img src="https://avatars.githubusercontent.com/u/35658596?v=4?s=110" width="110px;" alt="ajsy"/><br /><sub><b>ajsy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ajsy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/t3easy"><img src="https://avatars.githubusercontent.com/u/3628035?v=4?s=110" width="110px;" alt="Jan Kiesewetter"/><br /><sub><b>Jan Kiesewetter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=t3easy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tetrachloromethane250"><img src="https://avatars.githubusercontent.com/u/79449630?v=4?s=110" width="110px;" alt="Tetrachloromethane250"/><br /><sub><b>Tetrachloromethane250</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.kajes.se/"><img src="https://avatars.githubusercontent.com/u/22004482?v=4?s=110" width="110px;" alt="Lars Kajes"/><br /><sub><b>Lars Kajes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kajes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Joly0"><img src="https://avatars.githubusercontent.com/u/13993216?v=4?s=110" width="110px;" alt="Joly0"/><br /><sub><b>Joly0</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Joly0" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/limeless"><img src="https://avatars.githubusercontent.com/u/1501022?v=4?s=110" width="110px;" alt="theburger"/><br /><sub><b>theburger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=limeless" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/deivishome"><img src="https://avatars.githubusercontent.com/u/36065681?v=4?s=110" width="110px;" alt="David Valin Alonso"/><br /><sub><b>David Valin Alonso</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=deivishome" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andreaci"><img src="https://avatars.githubusercontent.com/u/8290389?v=4?s=110" width="110px;" alt="andreaci"/><br /><sub><b>andreaci</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andreaci" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.jellesebreghts.be"><img src="https://avatars.githubusercontent.com/u/1828542?v=4?s=110" width="110px;" alt="Jelle Sebreghts"/><br /><sub><b>Jelle Sebreghts</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Jelle-S" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Skywalker-11"><img src="https://avatars.githubusercontent.com/u/11180862?v=4?s=110" width="110px;" alt="Michael Pietsch"/><br /><sub><b>Michael Pietsch</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sh1hab"><img src="https://avatars.githubusercontent.com/u/22068886?v=4?s=110" width="110px;" alt="Masudul Haque Shihab"/><br /><sub><b>Masudul Haque Shihab</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sh1hab" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.freedomdive.com/"><img src="https://avatars.githubusercontent.com/u/16099942?v=4?s=110" width="110px;" alt="Supapong Areeprasertkul"/><br /><sub><b>Supapong Areeprasertkul</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zybersup" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/psarossy"><img src="https://avatars.githubusercontent.com/u/207358?v=4?s=110" width="110px;" alt="Peter Sarossy"/><br /><sub><b>Peter Sarossy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=psarossy" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nepella"><img src="https://avatars.githubusercontent.com/u/11823649?v=4?s=110" width="110px;" alt="Renee Margaret McConahy"/><br /><sub><b>Renee Margaret McConahy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nepella" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JohnnyPicnic"><img src="https://avatars.githubusercontent.com/u/5553884?v=4?s=110" width="110px;" alt="JohnnyPicnic"/><br /><sub><b>JohnnyPicnic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markbrule"><img src="https://avatars.githubusercontent.com/u/8799594?v=4?s=110" width="110px;" alt="markbrule"/><br /><sub><b>markbrule</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=markbrule" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikecmpbll"><img src="https://avatars.githubusercontent.com/u/1962801?v=4?s=110" width="110px;" alt="Mike Campbell"/><br /><sub><b>Mike Campbell</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mikecmpbll" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tbrconnect"><img src="https://avatars.githubusercontent.com/u/11973217?v=4?s=110" width="110px;" alt="tbrconnect"/><br /><sub><b>tbrconnect</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tbrconnect" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kcoyo"><img src="https://avatars.githubusercontent.com/u/12447225?v=4?s=110" width="110px;" alt="kcoyo"/><br /><sub><b>kcoyo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kcoyo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://travismiller.com/"><img src="https://avatars.githubusercontent.com/u/494017?v=4?s=110" width="110px;" alt="Travis Miller"/><br /><sub><b>Travis Miller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=travismiller" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Delta5"><img src="https://avatars.githubusercontent.com/u/1975640?v=4?s=110" width="110px;" alt="Evan Taylor"/><br /><sub><b>Evan Taylor</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Delta5" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PetriAsi"><img src="https://avatars.githubusercontent.com/u/8735148?v=4?s=110" width="110px;" alt="Petri Asikainen"/><br /><sub><b>Petri Asikainen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PetriAsi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/derdeagle"><img src="https://avatars.githubusercontent.com/u/11424540?v=4?s=110" width="110px;" alt="derdeagle"/><br /><sub><b>derdeagle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=derdeagle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://wh0rd.org/"><img src="https://avatars.githubusercontent.com/u/176950?v=4?s=110" width="110px;" alt="Mike Frysinger"/><br /><sub><b>Mike Frysinger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vapier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AL4AL"><img src="https://avatars.githubusercontent.com/u/22044358?v=4?s=110" width="110px;" alt="ALPHA"/><br /><sub><b>ALPHA</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AL4AL" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.ifern.de"><img src="https://avatars.githubusercontent.com/u/1042587?v=4?s=110" width="110px;" alt="FliegenKLATSCH"/><br /><sub><b>FliegenKLATSCH</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerm"><img src="https://avatars.githubusercontent.com/u/442138?v=4?s=110" width="110px;" alt="Jeremy Price"/><br /><sub><b>Jeremy Price</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jerm" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Toreg87"><img src="https://avatars.githubusercontent.com/u/84392209?v=4?s=110" width="110px;" alt="Toreg87"/><br /><sub><b>Toreg87</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Toreg87" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Computroniks"><img src="https://avatars.githubusercontent.com/u/67638596?v=4?s=110" width="110px;" alt="Matthew Nickson"/><br /><sub><b>Matthew Nickson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Computroniks" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://jethron.id.au"><img src="https://avatars.githubusercontent.com/u/1646397?v=4?s=110" width="110px;" alt="Jethro Nederhof"/><br /><sub><b>Jethro Nederhof</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jethron" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/01ste02"><img src="https://avatars.githubusercontent.com/u/23289826?v=4?s=110" width="110px;" alt="Oskar Stenberg"/><br /><sub><b>Oskar Stenberg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=01ste02" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Robert-Azelis"><img src="https://avatars.githubusercontent.com/u/82208283?v=4?s=110" width="110px;" alt="Robert-Azelis"/><br /><sub><b>Robert-Azelis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Robert-Azelis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alwism"><img src="https://avatars.githubusercontent.com/u/60648387?v=4?s=110" width="110px;" alt="Alexander William Smith"/><br /><sub><b>Alexander William Smith</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=alwism" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.leitwerk.de/"><img src="https://avatars.githubusercontent.com/u/24418301?v=4?s=110" width="110px;" alt="LEITWERK AG"/><br /><sub><b>LEITWERK AG</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=leitwerk-ag" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.aboutcher.co.uk"><img src="https://avatars.githubusercontent.com/u/1911435?v=4?s=110" width="110px;" alt="Adam"/><br /><sub><b>Adam</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adamboutcher" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://snksrv.com"><img src="https://avatars.githubusercontent.com/u/16104273?v=4?s=110" width="110px;" alt="Ian"/><br /><sub><b>Ian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sneak-it" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://blog.bestlong.idv.tw/"><img src="https://avatars.githubusercontent.com/u/4023909?v=4?s=110" width="110px;" alt="Shao Yu-Lung (Allen)"/><br /><sub><b>Shao Yu-Lung (Allen)</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bestlong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Haxatron"><img src="https://avatars.githubusercontent.com/u/76475453?v=4?s=110" width="110px;" alt="Haxatron"/><br /><sub><b>Haxatron</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Haxatron" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PlaneNuts"><img src="https://avatars.githubusercontent.com/u/88776392?v=4?s=110" width="110px;" alt="PlaneNuts"/><br /><sub><b>PlaneNuts</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PlaneNuts" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://bjcpgd.cias.rit.edu"><img src="https://avatars.githubusercontent.com/u/3842948?v=4?s=110" width="110px;" alt="Bradley Coudriet"/><br /><sub><b>Bradley Coudriet</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=exula" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://daltondur.st"><img src="https://avatars.githubusercontent.com/u/21966173?v=4?s=110" width="110px;" alt="Dalton Durst"/><br /><sub><b>Dalton Durst</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://adagiohealth.org"><img src="https://avatars.githubusercontent.com/u/38761237?v=4?s=110" width="110px;" alt="Alex Janes"/><br /><sub><b>Alex Janes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adagioajanes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nuraeil"><img src="https://avatars.githubusercontent.com/u/32387849?v=4?s=110" width="110px;" alt="Nuraeil"/><br /><sub><b>Nuraeil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nuraeil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TenOfTens"><img src="https://avatars.githubusercontent.com/u/48162670?v=4?s=110" width="110px;" alt="TenOfTens"/><br /><sub><b>TenOfTens</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TenOfTens" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ditisjens.be/"><img src="https://avatars.githubusercontent.com/u/9415391?v=4?s=110" width="110px;" alt="waffle"/><br /><sub><b>waffle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=insert-waffle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qveensi"><img src="https://avatars.githubusercontent.com/u/19945501?v=4?s=110" width="110px;" alt="Yevhenii Huzii"/><br /><sub><b>Yevhenii Huzii</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=qveensi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/veenone"><img src="https://avatars.githubusercontent.com/u/3839381?v=4?s=110" width="110px;" alt="Achmad Fienan Rahardianto"/><br /><sub><b>Achmad Fienan Rahardianto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=veenone" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chrisweirich"><img src="https://avatars.githubusercontent.com/u/97299851?v=4?s=110" width="110px;" alt="Christian Weirich"/><br /><sub><b>Christian Weirich</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chrisweirich" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/denzfarid"><img src="https://avatars.githubusercontent.com/u/1294403?v=4?s=110" width="110px;" alt="denzfarid"/><br /><sub><b>denzfarid</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ntbutler-nbcs"><img src="https://avatars.githubusercontent.com/u/94018771?v=4?s=110" width="110px;" alt="ntbutler-nbcs"/><br /><sub><b>ntbutler-nbcs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://naveensrinivasan.dev"><img src="https://avatars.githubusercontent.com/u/172697?v=4?s=110" width="110px;" alt="Naveen"/><br /><sub><b>Naveen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=naveensrinivasan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikeroq"><img src="https://avatars.githubusercontent.com/u/55674383?v=4?s=110" width="110px;" alt="Mike Roquemore"/><br /><sub><b>Mike Roquemore</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mikeroq" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reederda"><img src="https://avatars.githubusercontent.com/u/7991086?v=4?s=110" width="110px;" alt="Daniel Reeder"/><br /><sub><b>Daniel Reeder</b></sub></a><br /><a href="#translation-reederda" title="Translation">🌍</a> <a href="#translation-reederda" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=reederda" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vickyjaura183"><img src="https://avatars.githubusercontent.com/u/109422491?v=4?s=110" width="110px;" alt="vickyjaura183"/><br /><sub><b>vickyjaura183</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vickyjaura183" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/julian-piehl"><img src="https://avatars.githubusercontent.com/u/32363424?v=4?s=110" width="110px;" alt="Peace"/><br /><sub><b>Peace</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=julian-piehl" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kylegordon"><img src="https://avatars.githubusercontent.com/u/231528?v=4?s=110" width="110px;" alt="Kyle Gordon"/><br /><sub><b>Kyle Gordon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kylegordon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.bfh.ch"><img src="https://avatars.githubusercontent.com/u/53009155?v=4?s=110" width="110px;" alt="Katharina Drexel"/><br /><sub><b>Katharina Drexel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sunflowerbofh" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://david.sferruzza.fr/"><img src="https://avatars.githubusercontent.com/u/1931963?v=4?s=110" width="110px;" alt="David Sferruzza"/><br /><sub><b>David Sferruzza</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dsferruzza" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rnelsonee"><img src="https://avatars.githubusercontent.com/u/19511639?v=4?s=110" width="110px;" alt="Rick Nelson"/><br /><sub><b>Rick Nelson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rnelsonee" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BasO12"><img src="https://avatars.githubusercontent.com/u/94169344?v=4?s=110" width="110px;" alt="BasO12"/><br /><sub><b>BasO12</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BasO12" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vautia"><img src="https://avatars.githubusercontent.com/u/111710123?v=4?s=110" width="110px;" alt="Vautia"/><br /><sub><b>Vautia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Vautia" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.littlehart.net/atthekeyboard"><img src="https://avatars.githubusercontent.com/u/28321?v=4?s=110" width="110px;" alt="Chris Hartjes"/><br /><sub><b>Chris Hartjes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chartjes" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geo-chen"><img src="https://avatars.githubusercontent.com/u/2404584?v=4?s=110" width="110px;" alt="geo-chen"/><br /><sub><b>geo-chen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=geo-chen" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nh314"><img src="https://avatars.githubusercontent.com/u/6006620?v=4?s=110" width="110px;" alt="Phan Nguyen"/><br /><sub><b>Phan Nguyen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nh314" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/StarlessNights"><img src="https://avatars.githubusercontent.com/u/115993812?v=4?s=110" width="110px;" alt="Iisakki Jaakkola"/><br /><sub><b>Iisakki Jaakkola</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=StarlessNights" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=110" width="110px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=eltociear" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lukasfehling"><img src="https://avatars.githubusercontent.com/u/56871540?v=4?s=110" width="110px;" alt="Lukas Fehling"/><br /><sub><b>Lukas Fehling</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lukasfehling" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fernando-almeida"><img src="https://avatars.githubusercontent.com/u/1975990?v=4?s=110" width="110px;" alt="Fernando Almeida"/><br /><sub><b>Fernando Almeida</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fernando-almeida" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/akemidx"><img src="https://avatars.githubusercontent.com/u/116301219?v=4?s=110" width="110px;" alt="akemidx"/><br /><sub><b>akemidx</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=akemidx" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://oguz.site"><img src="https://avatars.githubusercontent.com/u/144778?v=4?s=110" width="110px;" alt="Oguz Bilgic"/><br /><sub><b>Oguz Bilgic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=oguzbilgic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/scoo73r"><img src="https://avatars.githubusercontent.com/u/9262438?v=4?s=110" width="110px;" alt="Scooter Crawford"/><br /><sub><b>Scooter Crawford</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=scoo73r" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/subdriven"><img src="https://avatars.githubusercontent.com/u/5957345?v=4?s=110" width="110px;" alt="subdriven"/><br /><sub><b>subdriven</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=subdriven" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AndrewSav"><img src="https://avatars.githubusercontent.com/u/658865?v=4?s=110" width="110px;" alt="Andrew Savinykh"/><br /><sub><b>Andrew Savinykh</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AndrewSav" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://kenchan0130.github.io"><img src="https://avatars.githubusercontent.com/u/1155067?v=4?s=110" width="110px;" alt="Tadayuki Onishi"/><br /><sub><b>Tadayuki Onishi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kenchan0130" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/floschoepfer"><img src="https://avatars.githubusercontent.com/u/112496896?v=4?s=110" width="110px;" alt="Florian"/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=floschoepfer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://spencerlong.com"><img src="https://avatars.githubusercontent.com/u/7305753?v=4?s=110" width="110px;" alt="Spencer Long"/><br /><sub><b>Spencer Long</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=spencerrlongg" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcusmoore"><img src="https://avatars.githubusercontent.com/u/1141514?v=4?s=110" width="110px;" alt="Marcus Moore"/><br /><sub><b>Marcus Moore</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=marcusmoore" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mezzle"><img src="https://avatars.githubusercontent.com/u/570639?v=4?s=110" width="110px;" alt="Martin Meredith"/><br /><sub><b>Martin Meredith</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="http://dboth.de"><img src="https://avatars.githubusercontent.com/u/5731963?v=4?s=110" width="110px;" alt="dboth"/><br /><sub><b>dboth</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dboth" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zacharyfleck"><img src="https://avatars.githubusercontent.com/u/87536651?v=4?s=110" width="110px;" alt="Zachary Fleck"/><br /><sub><b>Zachary Fleck</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zacharyfleck" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vikaas-cyper"><img src="https://avatars.githubusercontent.com/u/74609912?v=4?s=110" width="110px;" alt="VIKAAS-A"/><br /><sub><b>VIKAAS-A</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vikaas-cyper" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ak-piracha"><img src="https://avatars.githubusercontent.com/u/88882041?v=4?s=110" width="110px;" alt="Abdul Kareem"/><br /><sub><b>Abdul Kareem</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ak-piracha" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NojoudAlshehri"><img src="https://avatars.githubusercontent.com/u/111287779?v=4?s=110" width="110px;" alt="NojoudAlshehri"/><br /><sub><b>NojoudAlshehri</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stefanstidlffg"><img src="https://avatars.githubusercontent.com/u/54367449?v=4?s=110" width="110px;" alt="Stefan Stidl"/><br /><sub><b>Stefan Stidl</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=stefanstidlffg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qay21"><img src="https://avatars.githubusercontent.com/u/87803479?v=4?s=110" width="110px;" alt="Quentin Aymard"/><br /><sub><b>Quentin Aymard</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=qay21" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cram42"><img src="https://avatars.githubusercontent.com/u/5396871?v=4?s=110" width="110px;" alt="Grant Le Roux"/><br /><sub><b>Grant Le Roux</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=cram42" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://@singrity"><img src="https://avatars.githubusercontent.com/u/58479551?v=4?s=110" width="110px;" alt="Bogdan"/><br /><sub><b>Bogdan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Singrity" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mmanjos"><img src="https://avatars.githubusercontent.com/u/3483684?v=4?s=110" width="110px;" alt="mmanjos"/><br /><sub><b>mmanjos</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mmanjos" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://azooz2014.github.io/"><img src="https://avatars.githubusercontent.com/u/7429229?v=4?s=110" width="110px;" alt="Abdelaziz Faki"/><br /><sub><b>Abdelaziz Faki</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Azooz2014" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bilias"><img src="https://avatars.githubusercontent.com/u/47315739?v=4?s=110" width="110px;" alt="bilias"/><br /><sub><b>bilias</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bilias" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/coach1988"><img src="https://avatars.githubusercontent.com/u/2565989?v=4?s=110" width="110px;" alt="coach1988"/><br /><sub><b>coach1988</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=coach1988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mauro-miatello"><img src="https://avatars.githubusercontent.com/u/11910225?v=4?s=110" width="110px;" alt="MrM"/><br /><sub><b>MrM</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mauro-miatello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/koiakoia"><img src="https://avatars.githubusercontent.com/u/60405354?v=4?s=110" width="110px;" alt="koiakoia"/><br /><sub><b>koiakoia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=koiakoia" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mustafa-online"><img src="https://avatars.githubusercontent.com/u/5323832?v=4?s=110" width="110px;" alt="Mustafa Online"/><br /><sub><b>Mustafa Online</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mustafa-online" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franceslui"><img src="https://avatars.githubusercontent.com/u/104601439?v=4?s=110" width="110px;" alt="franceslui"/><br /><sub><b>franceslui</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=franceslui" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Q4kK"><img src="https://avatars.githubusercontent.com/u/125313163?v=4?s=110" width="110px;" alt="Q4kK"/><br /><sub><b>Q4kK</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Q4kK" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/squintfox"><img src="https://avatars.githubusercontent.com/u/55590532?v=4?s=110" width="110px;" alt="squintfox"/><br /><sub><b>squintfox</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=squintfox" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jeffclay"><img src="https://avatars.githubusercontent.com/u/1380084?v=4?s=110" width="110px;" alt="Jeff Clay"/><br /><sub><b>Jeff Clay</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jeffclay" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PP-JN-RL"><img src="https://avatars.githubusercontent.com/u/52716446?v=4?s=110" width="110px;" alt="Phil J R"/><br /><sub><b>Phil J R</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PP-JN-RL" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.corelight.com/"><img src="https://avatars.githubusercontent.com/u/1496725?v=4?s=110" width="110px;" alt="i_virus"/><br /><sub><b>i_virus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chandanchowdhury" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gitgrimbo"><img src="https://avatars.githubusercontent.com/u/1020541?v=4?s=110" width="110px;" alt="Paul Grime"/><br /><sub><b>Paul Grime</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gitgrimbo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://leeporte.co.uk"><img src="https://avatars.githubusercontent.com/u/922815?v=4?s=110" width="110px;" alt="Lee Porte"/><br /><sub><b>Lee Porte</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=LeePorte" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bryanlopezinc"><img src="https://avatars.githubusercontent.com/u/23613427?v=4?s=110" width="110px;" alt="BRYAN "/><br /><sub><b>BRYAN </b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/U-H-T"><img src="https://avatars.githubusercontent.com/u/64061710?v=4?s=110" width="110px;" alt="U-H-T"/><br /><sub><b>U-H-T</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=U-H-T" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tyree"><img src="https://avatars.githubusercontent.com/u/5395363?v=4?s=110" width="110px;" alt="Matt Tyree"/><br /><sub><b>Matt Tyree</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Tyree" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://spoontux.net"><img src="https://avatars.githubusercontent.com/u/292081?v=4?s=110" width="110px;" alt="Florent Bervas"/><br /><sub><b>Florent Bervas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FlorentDotMe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ditscheri.com"><img src="https://avatars.githubusercontent.com/u/4498077?v=4?s=110" width="110px;" alt="Daniel Albertsen"/><br /><sub><b>Daniel Albertsen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dbakan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/r-xyz"><img src="https://avatars.githubusercontent.com/u/100710244?v=4?s=110" width="110px;" alt="r-xyz"/><br /><sub><b>r-xyz</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=r-xyz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DrekiDegga"><img src="https://avatars.githubusercontent.com/u/47491036?v=4?s=110" width="110px;" alt="Steven Mainor"/><br /><sub><b>Steven Mainor</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DrekiDegga" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/arne-kroeger"><img src="https://avatars.githubusercontent.com/u/65785975?v=4?s=110" width="110px;" alt="arne-kroeger"/><br /><sub><b>arne-kroeger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=arne-kroeger" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Glukose1"><img src="https://avatars.githubusercontent.com/u/167117705?v=4?s=110" width="110px;" alt="Glukose1"/><br /><sub><b>Glukose1</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Glukose1" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Scarzy"><img src="https://avatars.githubusercontent.com/u/1197791?v=4?s=110" width="110px;" alt="Scarzy"/><br /><sub><b>Scarzy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Scarzy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/setpill"><img src="https://avatars.githubusercontent.com/u/37372069?v=4?s=110" width="110px;" alt="setpill"/><br /><sub><b>setpill</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=setpill" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swift2512"><img src="https://avatars.githubusercontent.com/u/3755203?v=4?s=110" width="110px;" alt="swift2512"/><br /><sub><b>swift2512</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://darrenraineys.co.uk"><img src="https://avatars.githubusercontent.com/u/6136439?v=4?s=110" width="110px;" alt="Darren Rainey"/><br /><sub><b>Darren Rainey</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DarrenRainey" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/maciej-poleszczyk"><img src="https://avatars.githubusercontent.com/u/133033121?v=4?s=110" width="110px;" alt="maciej-poleszczyk"/><br /><sub><b>maciej-poleszczyk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sgross-emlix"><img src="https://avatars.githubusercontent.com/u/143394709?v=4?s=110" width="110px;" alt="Sebastian Groß"/><br /><sub><b>Sebastian Groß</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sgross-emlix" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AnouarTouati"><img src="https://avatars.githubusercontent.com/u/41107778?v=4?s=110" width="110px;" alt="Anouar Touati"/><br /><sub><b>Anouar Touati</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AnouarTouati" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aHVzY2g"><img src="https://avatars.githubusercontent.com/u/25596663?v=4?s=110" width="110px;" alt="aHVzY2g"/><br /><sub><b>aHVzY2g</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=aHVzY2g" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://brlin.me"><img src="https://avatars.githubusercontent.com/u/13408130?v=4?s=110" width="110px;" alt="林博仁 Buo-ren Lin"/><br /><sub><b>林博仁 Buo-ren Lin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=brlin-tw" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://orbalia.pythonanywhere.com/"><img src="https://avatars.githubusercontent.com/u/18550946?v=4?s=110" width="110px;" alt="Adugna Gizaw"/><br /><sub><b>Adugna Gizaw</b></sub></a><br /><a href="#translation-addex12" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jostrander"><img src="https://avatars.githubusercontent.com/u/760989?v=4?s=110" width="110px;" alt="Jesse Ostrander"/><br /><sub><b>Jesse Ostrander</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jostrander" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/azmcnutt"><img src="https://avatars.githubusercontent.com/u/31522486?v=4?s=110" width="110px;" alt="James M"/><br /><sub><b>James M</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=azmcnutt" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fiala06"><img src="https://avatars.githubusercontent.com/u/5183146?v=4?s=110" width="110px;" alt="Fiala06"/><br /><sub><b>Fiala06</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Fiala06" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ntaylor-86"><img src="https://avatars.githubusercontent.com/u/28693782?v=4?s=110" width="110px;" alt="Nathan Taylor"/><br /><sub><b>Nathan Taylor</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ntaylor-86" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fvollmer"><img src="https://avatars.githubusercontent.com/u/16699443?v=4?s=110" width="110px;" alt="fvollmer"/><br /><sub><b>fvollmer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fvollmer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/36864"><img src="https://avatars.githubusercontent.com/u/109086466?v=4?s=110" width="110px;" alt="36864"/><br /><sub><b>36864</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=36864" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://clockwerx.blogspot.com/"><img src="https://avatars.githubusercontent.com/u/365751?v=4?s=110" width="110px;" alt="Daniel O'Connor"/><br /><sub><b>Daniel O'Connor</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=CloCkWeRX" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BeatSpark"><img src="https://avatars.githubusercontent.com/u/102852568?v=4?s=110" width="110px;" alt="BeatSpark"/><br /><sub><b>BeatSpark</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BeatSpark" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mrdahbi"><img src="https://avatars.githubusercontent.com/u/59203607?v=4?s=110" width="110px;" alt="mrdahbi"/><br /><sub><b>mrdahbi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mrdahbi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://sr.solutions"><img src="https://avatars.githubusercontent.com/u/6661332?v=4?s=110" width="110px;" alt="Fabian Schmid"/><br /><sub><b>Fabian Schmid</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chfsx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.chrisolin.com"><img src="https://avatars.githubusercontent.com/u/1288116?v=4?s=110" width="110px;" alt="Chris Olin"/><br /><sub><b>Chris Olin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=realchrisolin" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mnemonicly"><img src="https://avatars.githubusercontent.com/u/3803132?v=4?s=110" width="110px;" alt="Dan"/><br /><sub><b>Dan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mnemonicly" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NebelKreis"><img src="https://avatars.githubusercontent.com/u/43917728?v=4?s=110" width="110px;" alt="Nebel"/><br /><sub><b>Nebel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NebelKreis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/test1337ahp"><img src="https://avatars.githubusercontent.com/u/132433803?v=4?s=110" width="110px;" alt="test1337ahp"/><br /><sub><b>test1337ahp</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=test1337ahp" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JonathonReinhart"><img src="https://avatars.githubusercontent.com/u/1916566?v=4?s=110" width="110px;" alt="Jonathon Reinhart"/><br /><sub><b>Jonathon Reinhart</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JonathonReinhart" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aranar-pro"><img src="https://avatars.githubusercontent.com/u/484742?v=4?s=110" width="110px;" alt="aranar-pro"/><br /><sub><b>aranar-pro</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=aranar-pro" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/phil-flip"><img src="https://avatars.githubusercontent.com/u/27019397?v=4?s=110" width="110px;" alt="Phil"/><br /><sub><b>Phil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=phil-flip" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://fe80.fr/"><img src="https://avatars.githubusercontent.com/u/6473460?v=4?s=110" width="110px;" alt="Steffy Fort"/><br /><sub><b>Steffy Fort</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fe80" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sorvani"><img src="https://avatars.githubusercontent.com/u/3302372?v=4?s=110" width="110px;" alt="Jared Busch"/><br /><sub><b>Jared Busch</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sorvani" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/seanborg-codethink"><img src="https://avatars.githubusercontent.com/u/111956991?v=4?s=110" width="110px;" alt="seanborg-codethink"/><br /><sub><b>seanborg-codethink</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=seanborg-codethink" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dkaatz"><img src="https://avatars.githubusercontent.com/u/160669961?v=4?s=110" width="110px;" alt="dkaatz"/><br /><sub><b>dkaatz</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dkaatz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://threema.id/74SF7MW6?text="><img src="https://avatars.githubusercontent.com/u/827205?v=4?s=110" width="110px;" alt="Daniel Ruf"/><br /><sub><b>Daniel Ruf</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DanielRuf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahpaleus"><img src="https://avatars.githubusercontent.com/u/38883201?v=4?s=110" width="110px;" alt="ahpaleus"/><br /><sub><b>ahpaleus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ahpaleus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mink-adao-duy"><img src="https://avatars.githubusercontent.com/u/22906055?v=4?s=110" width="110px;" alt="Anh DAO-DUY"/><br /><sub><b>Anh DAO-DUY</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mink-adao-duy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Serdnad"><img src="https://avatars.githubusercontent.com/u/4723453?v=4?s=110" width="110px;" alt="Andres Gutierrez"/><br /><sub><b>Andres Gutierrez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Serdnad" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wewhite"><img src="https://avatars.githubusercontent.com/u/111083379?v=4?s=110" width="110px;" alt="Warren White"/><br /><sub><b>Warren White</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=wewhite" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://robintemme.de/"><img src="https://avatars.githubusercontent.com/u/2809241?v=4?s=110" width="110px;" alt="Robin Temme"/><br /><sub><b>Robin Temme</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=robintemme" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/herroworrd"><img src="https://avatars.githubusercontent.com/u/47008367?v=4?s=110" width="110px;" alt="herroworrd"/><br /><sub><b>herroworrd</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=herroworrd" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mubiu.com/"><img src="https://avatars.githubusercontent.com/u/28558609?v=4?s=110" width="110px;" alt="vicleos"/><br /><sub><b>vicleos</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vicleos" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://thinkl33t.co.uk/"><img src="https://avatars.githubusercontent.com/u/1016780?v=4?s=110" width="110px;" alt="Bob Clough"/><br /><sub><b>Bob Clough</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thinkl33t" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brandon-bailey"><img src="https://avatars.githubusercontent.com/u/10648463?v=4?s=110" width="110px;" alt="Brandon Daniel Bailey"/><br /><sub><b>Brandon Daniel Bailey</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=brandon-bailey" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcquark"><img src="https://avatars.githubusercontent.com/u/23556080?v=4?s=110" width="110px;" alt="Marc Bartelt"/><br /><sub><b>Marc Bartelt</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=marcquark" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/manu-crealytics"><img src="https://avatars.githubusercontent.com/u/18286893?v=4?s=110" width="110px;" alt="manu-crealytics"/><br /><sub><b>manu-crealytics</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=manu-crealytics" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.galaxy102.de/"><img src="https://avatars.githubusercontent.com/u/18245993?v=4?s=110" width="110px;" alt="Konstantin Köhring"/><br /><sub><b>Konstantin Köhring</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Galaxy102" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://deloz.net/"><img src="https://avatars.githubusercontent.com/u/685167?v=4?s=110" width="110px;" alt="Deloz"/><br /><sub><b>Deloz</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=deloz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mbrrg"><img src="https://avatars.githubusercontent.com/u/2682426?v=4?s=110" width="110px;" alt="Martin Berg"/><br /><sub><b>Martin Berg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mbrrg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Nothing4You"><img src="https://avatars.githubusercontent.com/u/3694534?v=4?s=110" width="110px;" alt="Richard Schwab"/><br /><sub><b>Richard Schwab</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Nothing4You" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://rickheil.com/"><img src="https://avatars.githubusercontent.com/u/8959676?v=4?s=110" width="110px;" alt="Rick Heil"/><br /><sub><b>Rick Heil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rickheil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rosscdh"><img src="https://avatars.githubusercontent.com/u/397106?v=4?s=110" width="110px;" alt="Ross Crawford-d'Heureuse"/><br /><sub><b>Ross Crawford-d'Heureuse</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rosscdh" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/McG800"><img src="https://avatars.githubusercontent.com/u/1621107?v=4?s=110" width="110px;" alt="Ryan McGuire"/><br /><sub><b>Ryan McGuire</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=McG800" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SBrown2021"><img src="https://avatars.githubusercontent.com/u/77835667?v=4?s=110" width="110px;" alt="SBrown2021"/><br /><sub><b>SBrown2021</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=SBrown2021" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/serkanerip"><img src="https://avatars.githubusercontent.com/u/8780913?v=4?s=110" width="110px;" alt="Serkan"/><br /><sub><b>Serkan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=serkanerip" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.yudelei.com/"><img src="https://avatars.githubusercontent.com/u/63188620?v=4?s=110" width="110px;" alt="Shanks"/><br /><sub><b>Shanks</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Shankschn" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cendai-mis"><img src="https://avatars.githubusercontent.com/u/198525698?v=4?s=110" width="110px;" alt="cendai-mis"/><br /><sub><b>cendai-mis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=cendai-mis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://smcpeck.github.io/"><img src="https://avatars.githubusercontent.com/u/8724583?v=4?s=110" width="110px;" alt="Shaun McPeck"/><br /><sub><b>Shaun McPeck</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=smcpeck" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/snazy2000"><img src="https://avatars.githubusercontent.com/u/1378836?v=4?s=110" width="110px;" alt="Stephen"/><br /><sub><b>Stephen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=snazy2000" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://nevets82.github.io/"><img src="https://avatars.githubusercontent.com/u/4462739?v=4?s=110" width="110px;" alt="Steven"/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Nevets82" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mateusvillar.com/"><img src="https://avatars.githubusercontent.com/u/29017267?v=4?s=110" width="110px;" alt="Mateus Villar"/><br /><sub><b>Mateus Villar</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Mateus-Romera" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mzack5020"><img src="https://avatars.githubusercontent.com/u/12749393?v=4?s=110" width="110px;" alt="Matthew Zackschewski"/><br /><sub><b>Matthew Zackschewski</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mzack5020" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.frei.media/"><img src="https://avatars.githubusercontent.com/u/12660103?v=4?s=110" width="110px;" alt="Matthias Frei"/><br /><sub><b>Matthias Frei</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=firefrei" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nticaric"><img src="https://avatars.githubusercontent.com/u/824840?v=4?s=110" width="110px;" alt="Nenad Ticaric"/><br /><sub><b>Nenad Ticaric</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nticaric" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Scorcher"><img src="https://avatars.githubusercontent.com/u/706439?v=4?s=110" width="110px;" alt="Nikolay Didenko"/><br /><sub><b>Nikolay Didenko</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Scorcher" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nunomaduro.com/sponsorships"><img src="https://avatars.githubusercontent.com/u/5457236?v=4?s=110" width="110px;" alt="Nuno Maduro"/><br /><sub><b>Nuno Maduro</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nunomaduro" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://tektikhq.com/"><img src="https://avatars.githubusercontent.com/u/8883074?v=4?s=110" width="110px;" alt="Oliver Walerys"/><br /><sub><b>Oliver Walerys</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=owalerys" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://keybase.io/rcmcdonald91"><img src="https://avatars.githubusercontent.com/u/3102039?v=4?s=110" width="110px;" alt="R. Christian McDonald"/><br /><sub><b>R. Christian McDonald</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rcmcdonald91" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nnix.net/"><img src="https://avatars.githubusercontent.com/u/1525581?v=4?s=110" width="110px;" alt="nix"/><br /><sub><b>nix</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nixn" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/octobunny"><img src="https://avatars.githubusercontent.com/u/55462380?v=4?s=110" width="110px;" alt="octobunny"/><br /><sub><b>octobunny</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=octobunny" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sreyemnayr"><img src="https://avatars.githubusercontent.com/u/8558670?v=4?s=110" width="110px;" alt="Ryan"/><br /><sub><b>Ryan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sreyemnayr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://benji.ltd/"><img src="https://avatars.githubusercontent.com/u/1501022?v=4?s=110" width="110px;" alt="p3nj"/><br /><sub><b>p3nj</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=p3nj" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/timwsuqld"><img src="https://avatars.githubusercontent.com/u/6201617?v=4?s=110" width="110px;" alt="Tim White"/><br /><sub><b>Tim White</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=timwsuqld" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yannikp"><img src="https://avatars.githubusercontent.com/u/22473767?v=4?s=110" width="110px;" alt="yannikp"/><br /><sub><b>yannikp</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=yannikp" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/viclou"><img src="https://avatars.githubusercontent.com/u/20525448?v=4?s=110" width="110px;" alt="victoria"/><br /><sub><b>victoria</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=viclou" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/valentyntu"><img src="https://avatars.githubusercontent.com/u/40685314?v=4?s=110" width="110px;" alt="Valentyn Tulub"/><br /><sub><b>Valentyn Tulub</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=valentyntu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://wouter0100.nl/"><img src="https://avatars.githubusercontent.com/u/864520?v=4?s=110" width="110px;" alt="Wouter van Os"/><br /><sub><b>Wouter van Os</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Wouter0100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/wyatt-teeter"><img src="https://avatars.githubusercontent.com/u/3946540?v=4?s=110" width="110px;" alt="Wyatt Teeter"/><br /><sub><b>Wyatt Teeter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=xWyatt" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/terwey"><img src="https://avatars.githubusercontent.com/u/1596124?v=4?s=110" width="110px;" alt="Yorick Terweijden"/><br /><sub><b>Yorick Terweijden</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=terwey" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bmkalle"><img src="https://avatars.githubusercontent.com/u/69298836?v=4?s=110" width="110px;" alt="bmkalle"/><br /><sub><b>bmkalle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bmkalle" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bricelabelle"><img src="https://avatars.githubusercontent.com/u/28403467?v=4?s=110" width="110px;" alt="bricelabelle"/><br /><sub><b>bricelabelle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bricelabelle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/corydlamb"><img src="https://avatars.githubusercontent.com/u/97770090?v=4?s=110" width="110px;" alt="corydlamb"/><br /><sub><b>corydlamb</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=corydlamb" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/splash"><img src="https://avatars.githubusercontent.com/u/1154133?v=4?s=110" width="110px;" alt="Diogenes S. Jesus"/><br /><sub><b>Diogenes S. Jesus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=splashx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dkmansion"><img src="https://avatars.githubusercontent.com/u/5826629?v=4?s=110" width="110px;" alt="D M"/><br /><sub><b>D M</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dkmansion" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jarli01"><img src="https://avatars.githubusercontent.com/u/14837699?v=4?s=110" width="110px;" alt="Dustin B"/><br /><sub><b>Dustin B</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Jarli01" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fabiang"><img src="https://avatars.githubusercontent.com/u/348344?v=4?s=110" width="110px;" alt="Fabian Grutschus"/><br /><sub><b>Fabian Grutschus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fabiang" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MelonSmasher"><img src="https://avatars.githubusercontent.com/u/1491053?v=4?s=110" width="110px;" alt="MelonSmasher"/><br /><sub><b>MelonSmasher</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=MelonSmasher" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AlexanderWPapyrus"><img src="https://avatars.githubusercontent.com/u/80526133?v=4?s=110" width="110px;" alt="AlexanderWPapyrus"/><br /><sub><b>AlexanderWPapyrus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AlexanderWPapyrus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/disc"><img src="https://avatars.githubusercontent.com/u/306231?v=4?s=110" width="110px;" alt="Alexandr Hacicheant"/><br /><sub><b>Alexandr Hacicheant</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=disc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://hex128.io/"><img src="https://avatars.githubusercontent.com/u/3032891?v=4?s=110" width="110px;" alt="Hex"/><br /><sub><b>Hex</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hex128" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/arukompas"><img src="https://avatars.githubusercontent.com/u/8697942?v=4?s=110" width="110px;" alt="Arunas Skirius"/><br /><sub><b>Arunas Skirius</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=arukompas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benperiton"><img src="https://avatars.githubusercontent.com/u/104396?v=4?s=110" width="110px;" alt="Ben Periton"/><br /><sub><b>Ben Periton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benperiton" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://wolfman.dev/"><img src="https://avatars.githubusercontent.com/u/11906832?v=4?s=110" width="110px;" alt="Byron Wolfman"/><br /><sub><b>Byron Wolfman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=byronwolfman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CalvinSchwartz"><img src="https://avatars.githubusercontent.com/u/56485508?v=4?s=110" width="110px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=CalvinSchwartz" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/juanfont"><img src="https://avatars.githubusercontent.com/u/181059?v=4?s=110" width="110px;" alt="Juan Font"/><br /><sub><b>Juan Font</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=juanfont" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/juhotaipale"><img src="https://avatars.githubusercontent.com/u/13137708?v=4?s=110" width="110px;" alt="Juho Taipale"/><br /><sub><b>Juho Taipale</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=juhotaipale" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KorvinSzanto"><img src="https://avatars.githubusercontent.com/u/1007419?v=4?s=110" width="110px;" alt="Korvin Szanto"/><br /><sub><b>Korvin Szanto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=KorvinSzanto" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://lewisfoster.foo/"><img src="https://avatars.githubusercontent.com/u/8513053?v=4?s=110" width="110px;" alt="Lewis Foster"/><br /><sub><b>Lewis Foster</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sniff122" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loganswartz"><img src="https://avatars.githubusercontent.com/u/33877541?v=4?s=110" width="110px;" alt="Logan Swartzendruber"/><br /><sub><b>Logan Swartzendruber</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=loganswartz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lopezio"><img src="https://avatars.githubusercontent.com/u/1156208?v=4?s=110" width="110px;" alt="Lorenzo P."/><br /><sub><b>Lorenzo P.</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lopezio" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/m4us1ne"><img src="https://avatars.githubusercontent.com/u/33946590?v=4?s=110" width="110px;" alt="Lukas Jung"/><br /><sub><b>Lukas Jung</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=m4us1ne" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://leafedfox.xyz/"><img src="https://avatars.githubusercontent.com/u/10965027?v=4?s=110" width="110px;" alt="Ellie"/><br /><sub><b>Ellie</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=LeafedFox" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gastamper"><img src="https://avatars.githubusercontent.com/u/20960555?v=4?s=110" width="110px;" alt="GA Stamper"/><br /><sub><b>GA Stamper</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gastamper" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gl-pup"><img src="https://avatars.githubusercontent.com/u/206553556?v=4?s=110" width="110px;" alt="Guillaume Lefranc"/><br /><sub><b>Guillaume Lefranc</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gl-pup" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dasjoe"><img src="https://avatars.githubusercontent.com/u/733892?v=4?s=110" width="110px;" alt="Hajo Möller"/><br /><sub><b>Hajo Möller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dasjoe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pottom"><img src="https://avatars.githubusercontent.com/u/3420063?v=4?s=110" width="110px;" alt="Istvan Basa"/><br /><sub><b>Istvan Basa</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=pottom" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://jjasghar.github.io/"><img src="https://avatars.githubusercontent.com/u/810824?v=4?s=110" width="110px;" alt="JJ Asghar"/><br /><sub><b>JJ Asghar</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jjasghar" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JemCdo"><img src="https://avatars.githubusercontent.com/u/40404495?v=4?s=110" width="110px;" alt="James E. Msenga"/><br /><sub><b>James E. Msenga</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JemCdo" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jfwiebe"><img src="https://avatars.githubusercontent.com/u/6865786?v=4?s=110" width="110px;" alt="Jan Felix Wiebe"/><br /><sub><b>Jan Felix Wiebe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jfwiebe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.nfon.com/"><img src="https://avatars.githubusercontent.com/u/43412008?v=4?s=110" width="110px;" alt="Jo Drexl"/><br /><sub><b>Jo Drexl</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=drexljo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/austinsasko"><img src="https://avatars.githubusercontent.com/u/4807843?v=4?s=110" width="110px;" alt="Austin Sasko"/><br /><sub><b>Austin Sasko</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=austinsasko" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END --> <!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,6 +1,6 @@
![snipe-it-by-grok](https://github.com/grokability/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602) ![snipe-it-by-grok](https://github.com/grokability/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://app.codacy.com/gh/snipe/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/grokability/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests.yml) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/804dd1beb14a41f38810ab77d64fc4fc)](https://app.codacy.com/gh/grokability/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/grokability/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
[![All Contributors](https://img.shields.io/badge/all_contributors-331-orange.svg?style=flat-square)](#contributing) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![All Contributors](https://img.shields.io/badge/all_contributors-331-orange.svg?style=flat-square)](#contributing) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk)
## Snipe-IT - Open Source Asset Management System ## Snipe-IT - Open Source Asset Management System

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class FixUpAssignedTypeWithoutAssignedTo extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:assigned-type-fixup';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes up assets that have an assigned_type but no assigned_to';
/**
* Execute the console command.
*/
public function handle()
{
DB::table('assets')->whereNotNull('assigned_type')->whereNull('assigned_to')->update(['assigned_type' => null]);
$this->info("Assets with an assigned_type but no assigned_to are fixed");
}
}

View File

@@ -182,7 +182,7 @@ class LdapSync extends Command
// Inject location information fields // Inject location information fields
for ($i = 0; $i < $results['count']; $i++) { for ($i = 0; $i < $results['count']; $i++) {
$results[$i]['ldap_location_override'] = false; $results[$i]['ldap_location_override'] = false;
$results[$i]['location_id'] = 0; $results[$i]['location_id'] = null;
} }
// Grab subsets based on location-specific DNs, and overwrite location for these users. // Grab subsets based on location-specific DNs, and overwrite location for these users.

View File

@@ -4,7 +4,7 @@ namespace App\Console\Commands;
use App\Models\Asset; use App\Models\Asset;
use App\Models\CustomField; use App\Models\CustomField;
use Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -66,8 +66,8 @@ class PaveIt extends Command
foreach ($custom_fields as $custom_field) { foreach ($custom_fields as $custom_field) {
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.'); $this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
if (\Schema::hasColumn('assets', $custom_field->db_column)) { if (Schema::hasColumn('assets', $custom_field->db_column)) {
\Schema::table('assets', function ($table) use ($custom_field) { Schema::table('assets', function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column); $table->dropColumn($custom_field->db_column);
}); });
} }
@@ -84,8 +84,8 @@ class PaveIt extends Command
} }
// Leave in the demo oauth keys so we don't have to reset them every day in the demos // Leave in the demo oauth keys so we don't have to reset them every day in the demos
\DB::statement('delete from oauth_clients WHERE id > 2'); DB::statement('delete from oauth_clients WHERE id > 2');
\DB::statement('delete from oauth_access_tokens WHERE id > 2'); DB::statement('delete from oauth_access_tokens WHERE user_id > 2');
} }
} }

View File

@@ -329,9 +329,9 @@ class RestoreFromBackup extends Command
} }
} }
} }
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', 'avif' $good_extensions = config('filesystems.allowed_upload_extensions_array');
];
foreach (array_merge($private_files, $public_files) as $file) { foreach (array_merge($private_files, $public_files) as $file) {
$has_wildcard = (strpos($file, '*') !== false); $has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) { if ($has_wildcard) {

View File

@@ -99,8 +99,11 @@ class SendAcceptanceReminder extends Command
foreach ($no_email_list as $user) { foreach ($no_email_list as $user) {
$rows[] = [$user['id'], $user['name']]; $rows[] = [$user['id'], $user['name']];
} }
$this->info("The following users do not have an email address:");
$this->table($headers, $rows); if (!empty($rows)) {
$this->info("The following users do not have an email address:");
$this->table($headers, $rows);
}
return 0; return 0;
} }

View File

@@ -28,7 +28,7 @@ class CheckoutableCheckedIn
$this->checkedOutTo = $checkedOutTo; $this->checkedOutTo = $checkedOutTo;
$this->checkedInBy = $checkedInBy; $this->checkedInBy = $checkedInBy;
$this->note = $note; $this->note = $note;
$this->action_date = $action_date ?? date('Y-m-d'); $this->action_date = $action_date ?? date('Y-m-d H:i:s');
$this->originalValues = $originalValues; $this->originalValues = $originalValues;
} }
} }

View File

@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Log;
use Throwable; use Throwable;
use JsonException; use JsonException;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
@@ -107,18 +108,25 @@ class Handler extends ExceptionHandler
$statusCode = $e->getStatusCode(); $statusCode = $e->getStatusCode();
// API throttle requests are handled in the RouteServiceProvider configureRateLimiting() method, so we don't need to handle them here
switch ($e->getStatusCode()) { switch ($e->getStatusCode()) {
case '404': case '404':
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode . ' endpoint not found'), 404); return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode . ' endpoint not found'), 404);
case '429':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Too many requests'), 429);
case '405': case '405':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405); return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
default: default:
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode); return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
} }
} }
// This handles API validation exceptions that happen at the Form Request level, so they
// never even get to the controller where we normally nicely format JSON responses
if ($e instanceof ValidationException) {
$response = $this->invalidJson($request, $e);
return response()->json(Helper::formatStandardApiResponse('error', null, $e->errors()), 200);
}
} }
@@ -143,6 +151,10 @@ class Handler extends ExceptionHandler
$route = 'maintenances.index'; $route = 'maintenances.index';
} elseif ($route === 'licenseseats.index') { } elseif ($route === 'licenseseats.index') {
$route = 'licenses.index'; $route = 'licenses.index';
} elseif ($route === 'customfields.index') {
$route = 'fields.index';
} elseif ($route === 'customfieldsets.index') {
$route = 'fields.index';
} }
return redirect() return redirect()
@@ -201,6 +213,7 @@ class Handler extends ExceptionHandler
*/ */
public function register() public function register()
{ {
$this->reportable(function (Throwable $e) { $this->reportable(function (Throwable $e) {
// //
}); });

View File

@@ -13,6 +13,7 @@ use App\Models\Setting;
use App\Models\Statuslabel; use App\Models\Statuslabel;
use App\Models\License; use App\Models\License;
use App\Models\Location; use App\Models\Location;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\DecryptException;
use Carbon\Carbon; use Carbon\Carbon;
@@ -722,8 +723,8 @@ class Helper
// The check and message that the user is still using the deprecated version // The check and message that the user is still using the deprecated version
$deprecations = [ $deprecations = [
'ms_teams_deprecated' => array( 'ms_teams_deprecated' => array(
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows'), 'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows') && (Setting::getSettings()->webhook_selected === 'microsoft'),
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Jan 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'), 'message' => 'The Microsoft Teams webhook URL being used will be deprecated Dec 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
]; ];
// if item of concern is being used and its being used with the deprecated values return the notification array. // if item of concern is being used and its being used with the deprecated values return the notification array.
@@ -876,6 +877,48 @@ class Helper
return false; return false;
} }
/**
* Check if the file is a video, so we can show a preview
*
* @param File $file
* @return string | Boolean
* @author [B. Wetherington] [<bwetherington@grokability.com>]
* @since [v8.1.18]
*/
public static function checkUploadIsVideo($file)
{
$finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'video/mp4') || ($filetype == 'video/quicktime') || ($filetype == 'video/mpeg') || ($filetype == 'video/ogg') || ($filetype == 'video/webm') || ($filetype == 'video/x-msvide')) {
return $filetype;
}
return false;
}
/**
* Check if the file is audio, so we can show a preview
*
* @param File $file
* @return string | Boolean
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
*/
public static function checkUploadIsAudio($file)
{
$finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'audio/mpeg') || ($filetype == 'audio/ogg')) {
return $filetype;
}
return false;
}
/** /**
* Walks through the permissions in the permissions config file and determines if * Walks through the permissions in the permissions config file and determines if
* permissions are granted based on a $selected_arr array. * permissions are granted based on a $selected_arr array.
@@ -1170,6 +1213,15 @@ class Helper
// Misc // Misc
'pdf' => 'far fa-file-pdf', 'pdf' => 'far fa-file-pdf',
'lic' => 'far fa-save', 'lic' => 'far fa-save',
// video
'mov' => 'fa-solid fa-video',
'mp4' => 'fa-solid fa-video',
// audio
'ogg' => 'fa-solid fa-file-audio',
'mp3' => 'fa-solid fa-file-audio',
'wav' => 'fa-solid fa-file-audio',
]; ];
if ($extension && array_key_exists($extension, $allowedExtensionMap)) { if ($extension && array_key_exists($extension, $allowedExtensionMap)) {
@@ -1313,25 +1365,24 @@ class Helper
switch ($item) { switch ($item) {
case 'asset': case 'asset':
return 'fas fa-barcode'; return 'fas fa-barcode';
break;
case 'accessory': case 'accessory':
return 'fas fa-keyboard'; return 'fas fa-keyboard';
break;
case 'component': case 'component':
return 'fas fa-hdd'; return 'fas fa-hdd';
break;
case 'consumable': case 'consumable':
return 'fas fa-tint'; return 'fas fa-tint';
break;
case 'license': case 'license':
return 'far fa-save'; return 'far fa-save';
break;
case 'location': case 'location':
return 'fas fa-map-marker-alt'; return 'fas fa-map-marker-alt';
break;
case 'user': case 'user':
return 'fas fa-user'; return 'fas fa-user';
break; case 'supplier':
return 'fa-solid fa-store';
case 'manufacturer':
return 'fa-solid fa-building';
case 'category':
return 'fa-solid fa-table-columns';
} }
} }
@@ -1481,68 +1532,64 @@ class Helper
} }
static public function getRedirectOption($request, $id, $table, $item_id = null) static public function getRedirectOption($request, $id, $table, $item_id = null) : RedirectResponse
{ {
$redirect_option = Session::get('redirect_option'); $redirect_option = Session::get('redirect_option') ?? $request->redirect_option;
$checkout_to_type = Session::get('checkout_to_type'); $checkout_to_type = Session::get('checkout_to_type') ?? null;
$checkedInFrom = Session::get('checkedInFrom'); $checkedInFrom = Session::get('checkedInFrom');
$other_redirect = Session::get('other_redirect'); $other_redirect = Session::get('other_redirect');
$backUrl = Session::pull('back_url', route('home'));
// return to previous page
if ($redirect_option === 'back') {
if ($backUrl === route('home')) {
return redirect()->to($backUrl)
->with('warning', trans('general.page_error'));
}
return redirect()->to($backUrl);
}
// return to index // return to index
if ($redirect_option == 'index') { if ($redirect_option == 'index') {
switch ($table) { return match ($table) {
case "Assets": 'Assets' => redirect()->route('hardware.index'),
return route('hardware.index'); 'Users' => redirect()->route('users.index'),
case "Users": 'Licenses' => redirect()->route('licenses.index'),
return route('users.index'); 'Accessories' => redirect()->route('accessories.index'),
case "Licenses": 'Components' => redirect()->route('components.index'),
return route('licenses.index'); 'Consumables' => redirect()->route('consumables.index'),
case "Accessories": };
return route('accessories.index');
case "Components":
return route('components.index');
case "Consumables":
return route('consumables.index');
}
} }
// return to thing being assigned // return to thing being assigned
if ($redirect_option == 'item') { if ($redirect_option == 'item') {
switch ($table) { return match ($table) {
case "Assets": 'Assets' => redirect()->route('hardware.show', $id ?? $item_id),
return route('hardware.show', $id ?? $item_id); 'Users' => redirect()->route('users.show', $id ?? $item_id),
case "Users": 'Licenses' => redirect()->route('licenses.show', $id ?? $item_id),
return route('users.show', $id ?? $item_id); 'Accessories' => redirect()->route('accessories.show', $id ?? $item_id),
case "Licenses": 'Components' => redirect()->route('components.show', $id ?? $item_id),
return route('licenses.show', $id ?? $item_id); 'Consumables' => redirect()->route('consumables.show', $id ?? $item_id),
case "Accessories": };
return route('accessories.show', $id ?? $item_id);
case "Components":
return route('components.show', $id ?? $item_id);
case "Consumables":
return route('consumables.show', $id ?? $item_id);
}
} }
// return to assignment target // return to assignment target
if ($redirect_option == 'target') { if ($redirect_option == 'target') {
switch ($checkout_to_type) { return match ($checkout_to_type) {
case 'user': 'user' => redirect()->route('users.show', $request->assigned_user ?? $checkedInFrom),
return route('users.show', $request->assigned_user ?? $checkedInFrom); 'location' => redirect()->route('locations.show', $request->assigned_location ?? $checkedInFrom),
case 'location': 'asset' => redirect()->route('hardware.show', $request->assigned_asset ?? $checkedInFrom),
return route('locations.show', $request->assigned_location ?? $checkedInFrom); };
case 'asset':
return route('hardware.show', $request->assigned_asset ?? $checkedInFrom);
}
} }
// return to somewhere else // return to somewhere else
if ($redirect_option == 'other_redirect') { if ($redirect_option == 'other_redirect') {
switch ($other_redirect) { return match ($other_redirect) {
case 'audit': 'audit' => redirect()->route('assets.audit.due'),
return route('assets.audit.due'); 'model' => redirect()->route('models.show', $request->model_id),
} };
} }

View File

@@ -16,14 +16,14 @@ class StorageHelper
$disk = config('filesystems.default'); $disk = config('filesystems.default');
} }
switch (config("filesystems.disks.$disk.driver")) { switch (config("filesystems.disks.$disk.driver")) {
case 'local': case 'local':
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?! return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
case 's3': case 's3':
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess? return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
default: default:
return Storage::disk($disk)->download($filename); return Storage::disk($disk)->download($filename);
} }
} }
@@ -33,21 +33,29 @@ class StorageHelper
* to determine that they are safe to display inline. * to determine that they are safe to display inline.
* *
* @author <A. Gianotto> [<snipe@snipe.net]> * @author <A. Gianotto> [<snipe@snipe.net]>
* @since v7.0.14 * @since v7.0.14
* @param $file_with_path * @param $file_with_path
* @return bool * @return bool
*/ */
public static function allowSafeInline($file_with_path) { public static function allowSafeInline($file_with_path)
{
$allowed_inline = [ $allowed_inline = [
'pdf',
'svg',
'jpg',
'gif',
'svg',
'avif', 'avif',
'webp', 'gif',
'gif',
'jpg',
'mov',
'mp3',
'mp4',
'ogg',
'pdf',
'png', 'png',
'svg',
'svg',
'wav',
'webm',
'webp',
]; ];
@@ -59,10 +67,24 @@ class StorageHelper
} }
public static function getFiletype($file_with_path)
{
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path)) {
return pathinfo($file_with_path, PATHINFO_EXTENSION);
}
return null;
}
/** /**
* Decide whether to show the file inline or download it. * Decide whether to show the file inline or download it.
*/ */
public static function showOrDownloadFile($file, $filename) { public static function showOrDownloadFile($file, $filename)
{
$headers = []; $headers = [];

View File

@@ -83,7 +83,8 @@ class AccessoriesController extends Controller
// Was the accessory created? // Was the accessory created?
if ($accessory->save()) { if ($accessory->save()) {
// Redirect to the new accessory page // Redirect to the new accessory page
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.create.success')); return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.create.success'));
} }
return redirect()->back()->withInput()->withErrors($accessory->getErrors()); return redirect()->back()->withInput()->withErrors($accessory->getErrors());
@@ -167,7 +168,8 @@ class AccessoriesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($accessory->save()) { if ($accessory->save()) {
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.update.success')); return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.update.success'));
} }
} else { } else {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist')); return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
@@ -220,7 +222,10 @@ class AccessoriesController extends Controller
*/ */
public function show(Accessory $accessory) : View | RedirectResponse public function show(Accessory $accessory) : View | RedirectResponse
{ {
$accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessory->id); $accessory->loadCount('checkouts as checkouts_count');
$accessory->load(['adminuser' => fn($query) => $query->withTrashed()]);
$this->authorize('view', $accessory); $this->authorize('view', $accessory);
return view('accessories.view', compact('accessory')); return view('accessories.view', compact('accessory'));
} }

View File

@@ -78,7 +78,8 @@ class AccessoryCheckinController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.checkin.success')); return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.checkin.success'));
} }
// Redirect to the accessory management page with error // Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error')); return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error'));

View File

@@ -97,7 +97,7 @@ class AccessoryCheckoutController extends Controller
// Redirect to the new accessory page // Redirect to the new accessory page
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories')) return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.checkout.success')); ->with('success', trans('admin/accessories/message.checkout.success'));
} }
} }

View File

@@ -7,6 +7,7 @@ use App\Events\CheckoutDeclined;
use App\Events\ItemAccepted; use App\Events\ItemAccepted;
use App\Events\ItemDeclined; use App\Events\ItemDeclined;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Asset; use App\Models\Asset;
use App\Models\CheckoutAcceptance; use App\Models\CheckoutAcceptance;
@@ -20,9 +21,12 @@ use App\Models\License;
use App\Models\Component; use App\Models\Component;
use App\Models\Consumable; use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification; use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification; use App\Notifications\AcceptanceAssetDeclinedNotification;
use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController; use App\Http\Controllers\SettingsController;
@@ -148,6 +152,8 @@ class AcceptanceController extends Controller
} }
} }
$assigned_user = User::find($acceptance->assigned_to_id);
// this is horrible // this is horrible
switch($acceptance->checkoutable_type){ switch($acceptance->checkoutable_type){
case 'App\Models\Asset': case 'App\Models\Asset':
@@ -157,35 +163,30 @@ class AcceptanceController extends Controller
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist')); return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
} }
$display_model = $asset_model->name; $display_model = $asset_model->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break; break;
case 'App\Models\Accessory': case 'App\Models\Accessory':
$pdf_view_route ='account.accept.accept-accessory-eula'; $pdf_view_route ='account.accept.accept-accessory-eula';
$accessory = Accessory::find($item->id); $accessory = Accessory::find($item->id);
$display_model = $accessory->name; $display_model = $accessory->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break; break;
case 'App\Models\LicenseSeat': case 'App\Models\LicenseSeat':
$pdf_view_route ='account.accept.accept-license-eula'; $pdf_view_route ='account.accept.accept-license-eula';
$license = License::find($item->license_id); $license = License::find($item->license_id);
$display_model = $license->name; $display_model = $license->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break; break;
case 'App\Models\Component': 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); $component = Component::find($item->id);
$display_model = $component->name; $display_model = $component->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break; break;
case 'App\Models\Consumable': case 'App\Models\Consumable':
$pdf_view_route ='account.accept.accept-consumable-eula'; $pdf_view_route ='account.accept.accept-consumable-eula';
$consumable = Consumable::find($item->id); $consumable = Consumable::find($item->id);
$display_model = $consumable->name; $display_model = $consumable->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break; break;
} }
// if ($acceptance->checkoutable_type == 'App\Models\Asset') { // if ($acceptance->checkoutable_type == 'App\Models\Asset') {
@@ -226,7 +227,7 @@ class AcceptanceController extends Controller
'note' => $request->input('note'), 'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'), 'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'), 'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
'assigned_to' => $assigned_to, 'assigned_to' => $assigned_user->present()->fullName,
'company_name' => $branding_settings->site_name, 'company_name' => $branding_settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null, 'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo, 'logo' => $path_logo,
@@ -240,6 +241,19 @@ class AcceptanceController extends Controller
} }
$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 !='')) {
// Add the attachment for the signing user into the $data array
$data['file'] = $pdf_filename;
try {
$assigned_user->notify(new AcceptanceAssetAcceptedToUserNotification($data));
} catch (\Exception $e) {
Log::warning($e);
}
}
try { try {
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data)); $acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -337,6 +351,21 @@ class AcceptanceController extends Controller
$return_msg = trans('admin/users/message.declined'); $return_msg = trans('admin/users/message.declined');
} }
if ($acceptance->alert_on_response_id) {
try {
$recipient = User::find($acceptance->alert_on_response_id);
if ($recipient) {
Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
$acceptance,
$recipient,
$request->input('asset_acceptance') === 'accepted',
));
}
} catch (Exception $e) {
Log::warning($e);
}
}
return redirect()->to('account/accept')->with('success', $return_msg); return redirect()->to('account/accept')->with('success', $return_msg);

View File

@@ -1,200 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use App\Http\Transformers\UploadedFilesTransformer;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Illuminate\Http\Request;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function store(UploadFileRequest $request, $assetId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $asset);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assets')) {
Storage::makeDirectory('private_uploads/assets', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function list(Asset $asset, Request $request) : JsonResponse | array
{
$this->authorize('view', $asset);
$allowed_columns =
[
'id',
'filename',
'eol',
'notes',
'created_at',
'updated_at',
];
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')->where('item_type', '=', Asset::class)->where('item_id', '=', $asset->id);
if ($request->filled('search')) {
$files = $files->TextSearch($request->input('search'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $files->count()) ? $files->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
$files = $files->orderBy($sort, $order);
$files = $files->skip($offset)->take($limit)->get();
return (new UploadedFilesTransformer())->transformFiles($files, $files->count());
}
/**
* Check for permissions and display the file.
*
* @param int $assetId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function show(Asset $asset, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assets/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetId
* @param int $fileId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function destroy(Asset $asset, $fileId = null) : JsonResponse
{
$rel_path = 'private_uploads/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
}

View File

@@ -75,6 +75,7 @@ class AssetMaintenancesController extends Controller
'serial', 'serial',
'created_by', 'created_by',
'supplier', 'supplier',
'location',
'is_warranty', 'is_warranty',
'status_label', 'status_label',
]; ];
@@ -98,6 +99,9 @@ class AssetMaintenancesController extends Controller
case 'serial': case 'serial':
$maintenances = $maintenances->OrderByAssetSerial($order); $maintenances = $maintenances->OrderByAssetSerial($order);
break; break;
case 'location':
$maintenances = $maintenances->OrderLocationName($order);
break;
case 'status_label': case 'status_label':
$maintenances = $maintenances->OrderStatusName($order); $maintenances = $maintenances->OrderStatusName($order);
break; break;

View File

@@ -1,184 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\AssetModel;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\AssetModelsTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetModelFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $assetModel);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file);
$assetModel->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetmodel
* @since [v7.0.12]
* @author [r-xyz]
*/
public function list($assetmodel_id) : JsonResponse | array
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetmodel_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$assetmodel = AssetModel::with('uploads')->find($assetmodel_id);
$this->authorize('view', $assetmodel);
return (new AssetModelsTransformer)->transformAssetModelFiles($assetmodel, $assetmodel->uploads()->count());
}
/**
* Check for permissions and display the file.
*
* @param int $assetModelId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v7.0.12]
* @author [r-xyz]
*/
public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assetmodels/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetModelId
* @param int $fileId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function destroy($assetModelId = null, $fileId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$rel_path = 'private_uploads/assetmodels';
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('update', $assetModel);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
}

View File

@@ -85,6 +85,12 @@ class AssetModelsController extends Controller
$assetmodels = $assetmodels->where('models.model_number', '=', $request->input('model_number')); $assetmodels = $assetmodels->where('models.model_number', '=', $request->input('model_number'));
} }
if ($request->input('requestable') == 'true') {
$assetmodels = $assetmodels->where('models.requestable', '=', '1');
} elseif ($request->input('requestable') == 'false') {
$assetmodels = $assetmodels->where('models.requestable', '=', '0');
}
if ($request->filled('notes')) { if ($request->filled('notes')) {
$assetmodels = $assetmodels->where('models.notes', '=', $request->input('notes')); $assetmodels = $assetmodels->where('models.notes', '=', $request->input('notes'));
} }
@@ -148,7 +154,7 @@ class AssetModelsController extends Controller
$assetmodel = $request->handleImages($assetmodel); $assetmodel = $request->handleImages($assetmodel);
if ($assetmodel->save()) { if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.create.success'))); return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.create.success')));
} }
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors())); return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
@@ -201,7 +207,7 @@ class AssetModelsController extends Controller
$assetmodel = AssetModel::findOrFail($id); $assetmodel = AssetModel::findOrFail($id);
$assetmodel->fill($request->all()); $assetmodel->fill($request->all());
$assetmodel = $request->handleImages($assetmodel); $assetmodel = $request->handleImages($assetmodel);
/** /**
* Allow custom_fieldset_id to override and populate fieldset_id. * Allow custom_fieldset_id to override and populate fieldset_id.
* This is stupid, but required for legacy API support. * This is stupid, but required for legacy API support.
@@ -216,7 +222,7 @@ class AssetModelsController extends Controller
if ($assetmodel->save()) { if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.update.success'))); return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.update.success')));
} }
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors())); return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));

View File

@@ -114,6 +114,7 @@ class AssetsController extends Controller
'byod', 'byod',
'asset_eol_date', 'asset_eol_date',
'requestable', 'requestable',
'jobtitle',
]; ];
$filter = []; $filter = [];
@@ -395,6 +396,9 @@ class AssetsController extends Controller
case 'assigned_to': case 'assigned_to':
$assets->OrderAssigned($order); $assets->OrderAssigned($order);
break; break;
case 'jobtitle':
$assets->OrderByJobTitle($order);
break;
case 'created_by': case 'created_by':
$assets->OrderByCreatedByName($order); $assets->OrderByCreatedByName($order);
break; break;
@@ -1141,12 +1145,12 @@ class AssetsController extends Controller
} }
} }
// Validate custom fields // Invoke the validation to see if the audit will complete successfully
Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate(); $asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
// Validate the rest of the data before we turn off the event dispatcher // Validate the rest of the data before we turn off the event dispatcher
if ($asset->isInvalid()) { if ($asset->isInvalid()) {
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors())); return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag' => $asset->asset_tag], $asset->getErrors()));
} }

View File

@@ -56,7 +56,7 @@ class CategoriesController extends Controller
'notes', 'notes',
]) ])
->with('adminuser') ->with('adminuser')
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count'); ->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
/* /*
@@ -212,7 +212,7 @@ class CategoriesController extends Controller
public function destroy($id) : JsonResponse public function destroy($id) : JsonResponse
{ {
$this->authorize('delete', Category::class); $this->authorize('delete', Category::class);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id); $category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($id);
if (! $category->isDeletable()) { if (! $category->isDeletable()) {
return response()->json( return response()->json(

View File

@@ -43,7 +43,10 @@ class CompaniesController extends Controller
$companies = Company::withCount(['assets as assets_count' => function ($query) { $companies = Company::withCount(['assets as assets_count' => function ($query) {
$query->AssetsForShow(); $query->AssetsForShow();
}])->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count'); }])
->with('adminuser')
->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
if ($request->filled('search')) { if ($request->filled('search')) {
$companies->TextSearch($request->input('search')); $companies->TextSearch($request->input('search'));
@@ -119,6 +122,7 @@ class CompaniesController extends Controller
{ {
$this->authorize('view', Company::class); $this->authorize('view', Company::class);
$company = Company::findOrFail($id); $company = Company::findOrFail($id);
$this->authorize('view', $company);
return (new CompaniesTransformer)->transformCompany($company); return (new CompaniesTransformer)->transformCompany($company);
} }
@@ -136,6 +140,7 @@ class CompaniesController extends Controller
{ {
$this->authorize('update', Company::class); $this->authorize('update', Company::class);
$company = Company::findOrFail($id); $company = Company::findOrFail($id);
$this->authorize('update', $company);
$company->fill($request->all()); $company->fill($request->all());
$company = $request->handleImages($company); $company = $request->handleImages($company);
@@ -188,6 +193,7 @@ class CompaniesController extends Controller
'companies.image', 'companies.image',
]); ]);
if ($request->filled('search')) { if ($request->filled('search')) {
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%'); $companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
} }

View File

@@ -234,6 +234,15 @@ class ImportController extends Controller
case 'location': case 'location':
$redirectTo = 'locations.index'; $redirectTo = 'locations.index';
break; break;
case 'supplier':
$redirectTo = 'suppliers.index';
break;
case 'manufacturer':
$redirectTo = 'manufacturers.index';
break;
case 'category':
$redirectTo = 'categories.index';
break;
} }
if ($errors) { //Failure if ($errors) { //Failure

View File

@@ -29,6 +29,15 @@ class LicenseSeatsController extends Controller
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department') $seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
->where('license_seats.license_id', $licenseId); ->where('license_seats.license_id', $licenseId);
if ($request->input('status') == 'available') {
$seats->whereNull('license_seats.assigned_to');
}
if ($request->input('status') == 'assigned') {
$seats->ByAssigned();
}
$order = $request->input('order') === 'asc' ? 'asc' : 'desc'; $order = $request->input('order') === 'asc' ? 'asc' : 'desc';
if ($request->input('sort') == 'department') { if ($request->input('sort') == 'department') {

View File

@@ -87,7 +87,8 @@ class LocationsController extends Controller
->withCount('accessories as accessories_count') ->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count') ->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count') ->withCount('children as children_count')
->withCount('users as users_count'); ->withCount('users as users_count')
->with('adminuser');
// Only scope locations if the setting is enabled // Only scope locations if the setting is enabled
if (Setting::getSettings()->scope_locations_fmcs) { if (Setting::getSettings()->scope_locations_fmcs) {
@@ -218,6 +219,7 @@ class LocationsController extends Controller
'locations.updated_at', 'locations.updated_at',
'locations.image', 'locations.image',
'locations.currency', 'locations.currency',
'locations.company_id',
'locations.notes', 'locations.notes',
]) ])
->withCount('assignedAssets as assigned_assets_count') ->withCount('assignedAssets as assigned_assets_count')

View File

@@ -4,15 +4,19 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Transformers\ProfileTransformer;
use App\Models\CheckoutRequest; use App\Models\CheckoutRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Laravel\Passport\TokenRepository; use Laravel\Passport\TokenRepository;
use Illuminate\Contracts\Validation\Factory as ValidationFactory; use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use App\Models\CustomField; use App\Models\CustomField;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ProfileController extends Controller class ProfileController extends Controller
{ {
@@ -167,6 +171,22 @@ class ProfileController extends Controller
} }
/**
* Display the EULAs accepted by the user.
*
* @param \App\Http\Transformers\ActionlogsTransformer $transformer
* @return \Illuminate\Http\JsonResponse
*@since [v8.1.16]
* @author [Godfrey Martinez] [<gmartinez@grokability.com>]
*/
public function eulas(ProfileTransformer $transformer)
{
// Only return this user's EULAs
$eulas = auth()->user()->eulas;
return response()->json(
$transformer->transformFiles($eulas, $eulas->count())
);
}
} }

View File

@@ -24,10 +24,15 @@ class SuppliersController extends Controller
public function index(Request $request): array public function index(Request $request): array
{ {
$this->authorize('view', Supplier::class); $this->authorize('view', Supplier::class);
$allowed_columns = [' $allowed_columns = [
id', 'id',
'name', 'name',
'address', 'address',
'address2',
'city',
'state',
'country',
'zip',
'phone', 'phone',
'contact', 'contact',
'fax', 'fax',
@@ -39,21 +44,24 @@ class SuppliersController extends Controller
'components_count', 'components_count',
'consumables_count', 'consumables_count',
'url', 'url',
'notes',
]; ];
$suppliers = Supplier::select( $suppliers = Supplier::select(
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'updated_at', 'deleted_at', 'image', 'notes', 'url']) ['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'created_by', 'updated_at', 'deleted_at', 'image', 'notes', 'url', 'zip'])
->withCount('assets as assets_count') ->withCount('assets as assets_count')
->withCount('licenses as licenses_count') ->withCount('licenses as licenses_count')
->withCount('accessories as accessories_count') ->withCount('accessories as accessories_count')
->withCount('components as components_count') ->withCount('components as components_count')
->withCount('consumables as consumables_count'); ->withCount('consumables as consumables_count')
->with('adminuser');
if ($request->filled('search')) { if ($request->filled('search')) {
$suppliers = $suppliers->TextSearch($request->input('search')); $suppliers->TextSearch($request->input('search'));
} }
if ($request->filled('name')) { if ($request->filled('name')) {
$suppliers->where('name', '=', $request->input('name')); $suppliers->where('name', '=', $request->input('name'));
} }
@@ -100,7 +108,15 @@ class SuppliersController extends Controller
$order = $request->input('order') === 'asc' ? 'asc' : 'desc'; $order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at'; $sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
$suppliers->orderBy($sort, $order);
switch ($request->input('sort')) {
case 'created_by':
$suppliers->OrderByCreatedByName($order);
break;
default:
$suppliers->orderBy($sort, $order);
break;
}
$total = $suppliers->count(); $total = $suppliers->count();
$suppliers = $suppliers->skip($offset)->take($limit)->get(); $suppliers = $suppliers->skip($offset)->take($limit)->get();

View File

@@ -0,0 +1,258 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\UploadedFilesTransformer;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Location;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class UploadedFilesController extends Controller
{
static $map_object_type = [
'accessories' => Accessory::class,
'assets' => Asset::class,
'components' => Component::class,
'consumables' => Consumable::class,
'hardware' => Asset::class,
'licenses' => License::class,
'locations' => Location::class,
'models' => AssetModel::class,
'users' => User::class,
];
static $map_storage_path = [
'accessories' => 'private_uploads/accessories/',
'assets' => 'private_uploads/assets/',
'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/',
'hardware' => 'private_uploads/assets/',
'licenses' => 'private_uploads/licenses/',
'locations' => 'private_uploads/locations/',
'models' => 'private_uploads/assetmodels/',
'users' => 'private_uploads/users/',
];
static $map_file_prefix= [
'accessories' => 'accessory',
'assets' => 'asset',
'components' => 'component',
'consumables' => 'consumable',
'hardware' => 'asset',
'licenses' => 'license',
'locations' => 'location',
'models' => 'model',
'users' => 'user',
];
/**
* List files for an object
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to list files for
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function index(Request $request, $object_type, $id) : JsonResponse | array
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Columns allowed for sorting
$allowed_columns =
[
'id',
'filename',
'action_type',
'note',
'created_at',
];
$uploads = $object->uploads();
$offset = ($request->input('offset') > $object->count()) ? $object->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'action_logs.created_at';
// Text search on action_logs fields
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searcghing across all relations
// And we generally won't need that here
if ($request->filled('search')) {
$uploads->where(
function ($query) use ($request) {
$query->where('filename', 'LIKE', '%' . $request->input('search') . '%')
->orWhere('note', 'LIKE', '%' . $request->input('search') . '%');
}
);
}
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
return (new UploadedFilesTransformer())->transformFiles($uploads, $uploads->count());
}
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to store so we can check permisisons
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $object_type, $id) : JsonResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// If the file storage directory doesn't exist, create it
if (! Storage::exists(self::$map_storage_path[$object_type])) {
Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
}
if ($request->hasFile('file')) {
// Loop over the attached files and add them to the object
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile(self::$map_storage_path[$object_type], self::$map_file_prefix[$object_type].'-'.$object->id, $file);
$files[] = $file_name;
$object->logUpload($file_name, $request->get('notes'));
}
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
->where('item_type', '=', self::$map_object_type[$object_type])
->where('item_id', '=', $id)->whereIn('filename', $files)
->get();
return response()->json(Helper::formatStandardApiResponse('success', (new UploadedFilesTransformer())->transformFiles($files, count($files)), trans_choice('general.file_upload_status.upload.success', count($files))));
}
// No files were submitted
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.nofiles')));
}
/**
* Check for permissions and display the file.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to delete from so we can check permisisons
* @param $file_id the ID of the file to delete from the action_logs table
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
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);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Check that the file being requested exists for the object
if (! $log = Actionlog::whereNotNull('filename')->where('item_type', self::$map_object_type[$object_type])->where('item_id', $object->id)->find($file_id)
) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_id')), 200);
}
if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.file_not_found'), 200));
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
}
return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
}
/**
* Delete the associated file
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to delete from so we can check permisisons
* @param $file_id the ID of the file to delete from the action_logs table
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function destroy($object_type, $id, $file_id) : JsonResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)->first();
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
}
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
}
}

View File

@@ -6,6 +6,7 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\SaveUserRequest; use App\Http\Requests\SaveUserRequest;
use App\Http\Transformers\AccessoriesTransformer; use App\Http\Transformers\AccessoriesTransformer;
use App\Http\Transformers\ActionlogsTransformer;
use App\Http\Transformers\AssetsTransformer; use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\ConsumablesTransformer; use App\Http\Transformers\ConsumablesTransformer;
use App\Http\Transformers\LicensesTransformer; use App\Http\Transformers\LicensesTransformer;
@@ -80,7 +81,7 @@ class UsersController extends Controller
'users.autoassign_licenses', 'users.autoassign_licenses',
'users.website', 'users.website',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations') ])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations', 'eulas')
->withCount([ ->withCount([
'assets as assets_count' => function(Builder $query) { 'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed(); $query->withoutTrashed();
@@ -206,11 +207,11 @@ class UsersController extends Controller
} }
if ($request->filled('manages_users_count')) { if ($request->filled('manages_users_count')) {
$users->has('manages_users_count', '=', $request->input('manages_users_count')); $users->has('managesUsers', '=', $request->input('manages_users_count'));
} }
if ($request->filled('manages_locations_count')) { if ($request->filled('manages_locations_count')) {
$users->has('manages_locations_count', '=', $request->input('manages_locations_count')); $users->has('managedLocations', '=', $request->input('manages_locations_count'));
} }
if ($request->filled('autoassign_licenses')) { if ($request->filled('autoassign_licenses')) {
@@ -676,7 +677,6 @@ class UsersController extends Controller
$this->authorize('view', License::class); $this->authorize('view', License::class);
if ($user = User::where('id', $id)->withTrashed()->first()) { if ($user = User::where('id', $id)->withTrashed()->first()) {
$this->authorize('update', $user);
$licenses = $user->licenses()->get(); $licenses = $user->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count()); return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
} }
@@ -736,6 +736,25 @@ class UsersController extends Controller
return (new UsersTransformer)->transformUser($request->user()); return (new UsersTransformer)->transformUser($request->user());
} }
/**
* Display the EULAs accepted by the user.
*
* @param \App\Models\User $user
* @param \App\Http\Transformers\ActionlogsTransformer $transformer
* @return \Illuminate\Http\JsonResponse
*@since [v8.1.16]
* @author [Godfrey Martinez] [<gmartinez@grokability.com>]
*/
public function eulas(User $user, ActionlogsTransformer $transformer)
{
$this->authorize('view', User::class);
$eulas = $user->eulas;
return response()->json(
$transformer->transformActionlogs($eulas, $eulas->count())
);
}
/** /**
* Restore a soft-deleted user. * Restore a soft-deleted user.
* *

View File

@@ -19,19 +19,6 @@ use \Illuminate\Http\RedirectResponse;
*/ */
class AssetMaintenancesController extends Controller class AssetMaintenancesController extends Controller
{ {
/**
* Checks for permissions for this action.
*
* @todo This should be replaced with middleware and/or policies
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
private static function getInsufficientPermissionsRedirect(): RedirectResponse
{
return redirect()->route('maintenances.index')
->with('error', trans('general.insufficient_permissions'));
}
/** /**
* Returns a view that invokes the ajax tables which actually contains * Returns a view that invokes the ajax tables which actually contains
@@ -67,16 +54,10 @@ class AssetMaintenancesController extends Controller
// We have to set this so that the correct property is set in the select2 ajax dropdown // We have to set this so that the correct property is set in the select2 ajax dropdown
$asset->asset_id = $asset->id; $asset->asset_id = $asset->id;
} }
// Prepare Asset Maintenance Type List
$assetMaintenanceType = [
'' => 'Select an asset maintenance type',
] + AssetMaintenance::getImprovementOptions();
// Mark the selected asset, if it came in
return view('asset_maintenances/edit') return view('asset_maintenances/edit')
->with('assetMaintenanceType', AssetMaintenance::getImprovementOptions())
->with('asset', $asset) ->with('asset', $asset)
->with('assetMaintenanceType', $assetMaintenanceType)
->with('item', new AssetMaintenance); ->with('item', new AssetMaintenance);
} }
@@ -91,43 +72,45 @@ class AssetMaintenancesController extends Controller
public function store(Request $request) : RedirectResponse public function store(Request $request) : RedirectResponse
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
// create a new model instance
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = $request->input('notes');
$asset = Asset::find($request->input('asset_id'));
if ((! Company::isCurrentUserHasAccess($asset)) && ($asset != null)) { $assets = Asset::whereIn('id', $request->input('selected_assets'))->get();
return static::getInsufficientPermissionsRedirect();
// Loop through the selected assets
foreach ($assets as $asset) {
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = $request->input('notes');
// Save the asset maintenance data
$assetMaintenance->asset_id = $asset->id;
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$assetMaintenance->created_by = auth()->id();
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
// Was the asset maintenance created?
if (!$assetMaintenance->save()) {
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
}
} }
// Save the asset maintenance data return redirect()->route('maintenances.index')
$assetMaintenance->asset_id = $request->input('asset_id'); ->with('success', trans('admin/asset_maintenances/message.create.success'));
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$assetMaintenance->created_by = auth()->id();
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
// Was the asset maintenance created?
if ($assetMaintenance->save()) {
// Redirect to the new asset maintenance page
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
} }
/** /**
@@ -135,26 +118,19 @@ class AssetMaintenancesController extends Controller
* *
* @see AssetMaintenancesController::postEdit() method that stores the data * @see AssetMaintenancesController::postEdit() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId
* @version v1.0 * @version v1.0
* @since [v1.8] * @since [v1.8]
*/ */
public function edit(AssetMaintenance $maintenance) : View | RedirectResponse public function edit(AssetMaintenance $maintenance) : View | RedirectResponse
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
if ((!$maintenance->asset) || ($maintenance->asset->deleted_at!='')) { $this->authorize('update', $maintenance->asset);
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
} elseif (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
// Prepare Improvement Type List
$assetMaintenanceType = ['' => 'Select an improvement type'] + AssetMaintenance::getImprovementOptions();
return view('asset_maintenances/edit') return view('asset_maintenances/edit')
->with('selectedAsset', null) ->with('selected_assets', $maintenance->asset->pluck('id')->toArray())
->with('assetMaintenanceType', $assetMaintenanceType) ->with('asset_ids', request()->input('asset_ids', []))
->with('item', $maintenance); ->with('assetMaintenanceType', AssetMaintenance::getImprovementOptions())
->with('item', $maintenance);
} }
/** /**
@@ -170,33 +146,21 @@ class AssetMaintenancesController extends Controller
public function update(Request $request, AssetMaintenance $maintenance) : View | RedirectResponse public function update(Request $request, AssetMaintenance $maintenance) : View | RedirectResponse
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
$this->authorize('update', $maintenance->asset);
if ((!$maintenance->asset) || ($maintenance->asset->deleted_at!='')) {
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
} elseif (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
$maintenance->supplier_id = $request->input('supplier_id'); $maintenance->supplier_id = $request->input('supplier_id');
$maintenance->is_warranty = $request->input('is_warranty'); $maintenance->is_warranty = $request->input('is_warranty', 0);
$maintenance->cost = $request->input('cost'); $maintenance->cost = $request->input('cost');
$maintenance->notes = $request->input('notes'); $maintenance->notes = $request->input('notes');
$asset = Asset::find(request('asset_id'));
if (! Company::isCurrentUserHasAccess($asset)) {
return static::getInsufficientPermissionsRedirect();
}
// Save the asset maintenance data
$maintenance->asset_id = $request->input('asset_id');
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type'); $maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$maintenance->title = $request->input('title'); $maintenance->title = $request->input('title');
$maintenance->start_date = $request->input('start_date'); $maintenance->start_date = $request->input('start_date');
$maintenance->completion_date = $request->input('completion_date'); $maintenance->completion_date = $request->input('completion_date');
if (($maintenance->completion_date == null)
) { // Todo - put this in a getter/setter?
if (($maintenance->completion_date == null))
{
if (($maintenance->asset_maintenance_time !== 0) if (($maintenance->asset_maintenance_time !== 0)
|| (! is_null($maintenance->asset_maintenance_time)) || (! is_null($maintenance->asset_maintenance_time))
) { ) {
@@ -213,10 +177,7 @@ class AssetMaintenancesController extends Controller
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true); $maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
} }
// Was the asset maintenance created?
if ($maintenance->save()) { if ($maintenance->save()) {
// Redirect to the new asset maintenance page
return redirect()->route('maintenances.index') return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.edit.success')); ->with('success', trans('admin/asset_maintenances/message.edit.success'));
} }
@@ -232,21 +193,12 @@ class AssetMaintenancesController extends Controller
* @version v1.0 * @version v1.0
* @since [v1.8] * @since [v1.8]
*/ */
public function destroy($assetMaintenanceId) : RedirectResponse public function destroy(AssetMaintenance $maintenance) : RedirectResponse
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
// Check if the asset maintenance exists $this->authorize('update', $maintenance->asset);
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
// Delete the asset maintenance // Delete the asset maintenance
$assetMaintenance->delete(); $maintenance->delete();
// Redirect to the asset_maintenance management page // Redirect to the asset_maintenance management page
return redirect()->route('maintenances.index') return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.delete.success')); ->with('success', trans('admin/asset_maintenances/message.delete.success'));
@@ -262,11 +214,6 @@ class AssetMaintenancesController extends Controller
*/ */
public function show(AssetMaintenance $maintenance) : View | RedirectResponse public function show(AssetMaintenance $maintenance) : View | RedirectResponse
{ {
$this->authorize('view', Asset::class);
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
return view('asset_maintenances/view')->with('assetMaintenance', $maintenance); return view('asset_maintenances/view')->with('assetMaintenance', $maintenance);
} }
} }

View File

@@ -96,7 +96,6 @@ class AssetCheckinController extends Controller
}); });
$asset->expected_checkin = null; $asset->expected_checkin = null;
$asset->last_checkin = now();
$asset->assignedTo()->disassociate($asset); $asset->assignedTo()->disassociate($asset);
$asset->accepted = null; $asset->accepted = null;
$asset->name = $request->get('name'); $asset->name = $request->get('name');
@@ -123,11 +122,14 @@ class AssetCheckinController extends Controller
$originalValues = $asset->getRawOriginal(); $originalValues = $asset->getRawOriginal();
// Handle last checkin date
$checkin_at = date('Y-m-d H:i:s'); $checkin_at = date('Y-m-d H:i:s');
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) { if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
$originalValues['action_date'] = $checkin_at; $originalValues['action_date'] = $checkin_at;
$checkin_at = $request->get('checkin_at'); $checkin_at = $request->get('checkin_at');
} }
$asset->last_checkin = $checkin_at;
$asset->licenseseats->each(function (LicenseSeat $seat) { $asset->licenseseats->each(function (LicenseSeat $seat) {
$seat->update(['assigned_to' => null]); $seat->update(['assigned_to' => null]);
@@ -151,7 +153,8 @@ class AssetCheckinController extends Controller
if ($asset->save()) { if ($asset->save()) {
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues)); event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))->with('success', trans('admin/hardware/message.checkin.success')); return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.checkin.success'));
} }
// Redirect to the asset management page with error // Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.error').$asset->getErrors()); return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.error').$asset->getErrors());

View File

@@ -123,7 +123,7 @@ class AssetCheckoutController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]); session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) { if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.checkout.success')); ->with('success', trans('admin/hardware/message.checkout.success'));
} }
// Redirect to the asset management page with error // Redirect to the asset management page with error

View File

@@ -149,7 +149,7 @@ class AssetsController extends Controller
$asset->byod = request('byod', 0); $asset->byod = request('byod', 0);
if (! empty($settings->audit_interval)) { if (! empty($settings->audit_interval)) {
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); $asset->next_audit_date = Carbon::now()->addMonths((int) $settings->audit_interval)->toDateString();
} }
// Set location_id to rtd_location_id ONLY if the asset isn't being checked out // Set location_id to rtd_location_id ONLY if the asset isn't being checked out
@@ -188,14 +188,31 @@ class AssetsController extends Controller
// Validate the asset before saving // Validate the asset before saving
if ($asset->isValid() && $asset->save()) { if ($asset->isValid() && $asset->save()) {
if (request('assigned_user')) { $target = null;
$target = User::find(request('assigned_user')); $location = null;
if ($userId = request('assigned_user')) {
$target = User::find($userId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.user'));
}
$location = $target->location_id; $location = $target->location_id;
} elseif (request('assigned_asset')) {
$target = Asset::find(request('assigned_asset')); } elseif ($assetId = request('assigned_asset')) {
$target = Asset::find($assetId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.asset'));
}
$location = $target->location_id; $location = $target->location_id;
} elseif (request('assigned_location')) {
$target = Location::find(request('assigned_location')); } elseif ($locationId = request('assigned_location')) {
$target = Location::find($locationId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.location'));
}
$location = $target->id; $location = $target->id;
} }
@@ -216,18 +233,18 @@ class AssetsController extends Controller
if ($successes) { if ($successes) {
if ($failures) { if ($failures) {
//some succeeded, some failed //some succeeded, some failed
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) //FIXME - not tested return Helper::getRedirectOption($request, $asset->id, 'Assets') //FIXME - not tested
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)])) ->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]))
->with('warning', trans_choice('admin/hardware/message.create.partial_failure', $failures, ['failures' => join("; ", $failures)])); ->with('warning', trans_choice('admin/hardware/message.create.partial_failure', $failures, ['failures' => join("; ", $failures)]));
} else { } else {
if (count($successes) == 1) { if (count($successes) == 1) {
//the most common case, keeping it so we don't have to make every use of that translation string be trans_choice'ed //the most common case, keeping it so we don't have to make every use of that translation string be trans_choice'ed
//and re-translated //and re-translated
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset), 'id', 'tag' => e($asset->asset_tag)])); ->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset), 'id', 'tag' => e($asset->asset_tag)]));
} else { } else {
//multi-success //multi-success
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)])); ->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]));
} }
} }
@@ -248,6 +265,7 @@ class AssetsController extends Controller
public function edit(Asset $asset) : View | RedirectResponse public function edit(Asset $asset) : View | RedirectResponse
{ {
$this->authorize($asset); $this->authorize($asset);
session()->put('back_url', url()->previous());
return view('hardware/edit') return view('hardware/edit')
->with('item', $asset) ->with('item', $asset)
->with('statuslabel_list', Helper::statusLabelList()) ->with('statuslabel_list', Helper::statusLabelList())
@@ -410,11 +428,15 @@ class AssetsController extends Controller
} }
} }
} }
session()->put([
'redirect_option' => $request->get('redirect_option'),
'checkout_to_type' => $request->get('checkout_to_type'),
'other_redirect' => $request->get('redirect_option') === 'other_redirect' ? 'model' : null,
]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($asset->save()) { if ($asset->save()) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.update.success')); ->with('success', trans('admin/hardware/message.update.success'));
} }
@@ -446,7 +468,7 @@ class AssetsController extends Controller
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on delete', $checkin_at, $originalValues)); event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on delete', $checkin_at, $originalValues));
DB::table('assets') DB::table('assets')
->where('id', $asset->id) ->where('id', $asset->id)
->update(['assigned_to' => null]); ->update(['assigned_to' => null, 'assigned_type' => null]);
} }
@@ -458,6 +480,7 @@ class AssetsController extends Controller
} }
} }
$asset->delete(); $asset->delete();
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success')); return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
@@ -890,7 +913,7 @@ class AssetsController extends Controller
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors()); return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
} }
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); $dt = Carbon::now()->addMonths( (int) $settings->audit_interval)->toDateString();
return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list'); return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list');
} }
@@ -937,8 +960,8 @@ class AssetsController extends Controller
} }
} }
// Validate custom fields // Invoke the validation to see if the audit will complete successfully
Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate(); $asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
// Validate the rest of the data before we turn off the event dispatcher // Validate the rest of the data before we turn off the event dispatcher
if ($asset->isInvalid()) { if ($asset->isInvalid()) {
@@ -978,7 +1001,7 @@ class AssetsController extends Controller
} }
$asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues); $asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues);
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))->with('success', trans('admin/hardware/message.audit.success')); return Helper::getRedirectOption($request, $asset->id, 'Assets')->with('success', trans('admin/hardware/message.audit.success'));
} }
return redirect()->back()->withInput()->withErrors($asset->getErrors()); return redirect()->back()->withInput()->withErrors($asset->getErrors());

View File

@@ -52,11 +52,26 @@ class BulkAssetsController extends Controller
} }
$asset_ids = $request->input('ids'); $asset_ids = $request->input('ids');
if ($request->input('bulk_actions') === 'checkout') { if ($request->input('bulk_actions') === 'checkout') {
$status_check =$this->hasUndeployableStatus($asset_ids);
if($status_check && $status_check['status'] === true){
$asset_tags = implode(', ', array_column($status_check['tags'], 'asset_tag'));
$asset_ids = $status_check['asset_ids'];
session()->flash('warning', trans('admin/hardware/message.undeployable', ['asset_tags' => $asset_tags]));
}
$request->session()->flashInput(['selected_assets' => $asset_ids]); $request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect()->route('hardware.bulkcheckout.show'); return redirect()->route('hardware.bulkcheckout.show');
} }
if ($request->input('bulk_actions') === 'maintenance') {
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect()->route('maintenances.create');
}
// Figure out where we need to send the user after the update is complete, and store that in the session // Figure out where we need to send the user after the update is complete, and store that in the session
$bulk_back_url = request()->headers->get('referer'); $bulk_back_url = request()->headers->get('referer');
session(['bulk_back_url' => $bulk_back_url]); session(['bulk_back_url' => $bulk_back_url]);
@@ -97,11 +112,47 @@ class BulkAssetsController extends Controller
// This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array) // This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array)
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id'; $column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id';
$assets = Asset::with('assignedTo', 'location', 'model') $query = Asset::with('assignedTo', 'location', 'model')
->whereIn('assets.id', $asset_ids) ->whereIn('assets.id', $asset_ids)
->withTrashed(); ->withTrashed();
$assets = $assets->get();
switch ($sort_override) {
case 'model':
$query->OrderModels($order);
break;
case 'model_number':
$query->OrderModelNumber($order);
break;
case 'category':
$query->OrderCategory($order);
break;
case 'manufacturer':
$query->OrderManufacturer($order);
break;
case 'company':
$query->OrderCompany($order);
break;
case 'location':
$query->OrderLocation($order);
break;
case 'rtd_location':
$query->OrderRtdLocation($order);
break;
case 'status_label':
$query->OrderStatus($order);
break;
case 'supplier':
$query->OrderSupplier($order);
break;
case 'assigned_to':
$query->OrderAssigned($order);
break;
default:
$query->orderBy($column_sort, $order);
break;
}
$assets = $query->get();
if ($assets->isEmpty()) { if ($assets->isEmpty()) {
Log::debug('No assets were found for the provided IDs', ['ids' => $asset_ids]); Log::debug('No assets were found for the provided IDs', ['ids' => $asset_ids]);
@@ -154,40 +205,7 @@ class BulkAssetsController extends Controller
} }
} }
switch ($sort_override) {
case 'model':
$assets->OrderModels($order);
break;
case 'model_number':
$assets->OrderModelNumber($order);
break;
case 'category':
$assets->OrderCategory($order);
break;
case 'manufacturer':
$assets->OrderManufacturer($order);
break;
case 'company':
$assets->OrderCompany($order);
break;
case 'location':
$assets->OrderLocation($order);
case 'rtd_location':
$assets->OrderRtdLocation($order);
break;
case 'status_label':
$assets->OrderStatus($order);
break;
case 'supplier':
$assets->OrderSupplier($order);
break;
case 'assigned_to':
$assets->OrderAssigned($order);
break;
default:
$assets->orderBy($column_sort, $order);
break;
}
return redirect()->back()->with('error', 'No action selected'); return redirect()->back()->with('error', 'No action selected');
} }
@@ -214,6 +232,21 @@ class BulkAssetsController extends Controller
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray(); $custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
// find custom field input attributes that start with 'null_'
$null_custom_fields_inputs = array_filter($request->all(), function ($key) {
// filter out all keys that start with 'null_'
return (strpos($key, 'null_') === 0);
}, ARRAY_FILTER_USE_KEY);;
// remove 'null' from the keys
$custom_fields_to_null = [];
foreach ($null_custom_fields_inputs as $key => $value) {
$custom_fields_to_null[str_replace('null', '', $key)] = $value;
}
if (! $request->filled('ids') || count($request->input('ids')) == 0) { if (! $request->filled('ids') || count($request->input('ids')) == 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected')); return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
@@ -251,7 +284,9 @@ class BulkAssetsController extends Controller
|| ($request->filled('null_expected_checkin_date')) || ($request->filled('null_expected_checkin_date'))
|| ($request->filled('null_next_audit_date')) || ($request->filled('null_next_audit_date'))
|| ($request->filled('null_asset_eol_date')) || ($request->filled('null_asset_eol_date'))
|| ($request->filled('null_notes'))
|| ($request->anyFilled($custom_field_columns)) || ($request->anyFilled($custom_field_columns))
|| ($request->anyFilled(array_keys($null_custom_fields_inputs)))
) { ) {
// Let's loop through those assets and build an update array // Let's loop through those assets and build an update array
@@ -274,10 +309,14 @@ class BulkAssetsController extends Controller
->conditionallyAddItem('supplier_id') ->conditionallyAddItem('supplier_id')
->conditionallyAddItem('warranty_months') ->conditionallyAddItem('warranty_months')
->conditionallyAddItem('next_audit_date') ->conditionallyAddItem('next_audit_date')
->conditionallyAddItem('asset_eol_date'); ->conditionallyAddItem('asset_eol_date')
->conditionallyAddItem('notes');
foreach ($custom_field_columns as $key => $custom_field_column) { foreach ($custom_field_columns as $key => $custom_field_column) {
$this->conditionallyAddItem($custom_field_column); $this->conditionallyAddItem($custom_field_column);
} }
foreach ($custom_fields_to_null as $key => $custom_field_to_null) {
$this->conditionallyAddItem($key);
}
if (!($asset->eol_explicit)) { if (!($asset->eol_explicit)) {
if ($request->filled('model_id')) { if ($request->filled('model_id')) {
@@ -328,6 +367,10 @@ class BulkAssetsController extends Controller
} }
} }
if ($request->input('null_notes')=='1') {
$this->update_array['notes'] = null;
}
if ($request->filled('purchase_cost')) { if ($request->filled('purchase_cost')) {
@@ -368,10 +411,12 @@ class BulkAssetsController extends Controller
// This could probably be added to a form request. // This could probably be added to a form request.
// If the asset isn't assigned, we don't care what the status is. // If the asset isn't assigned, we don't care what the status is.
// Otherwise we need to make sure the status type is still a deployable one. // Otherwise we need to make sure the status type is still a deployable one.
if (
($asset->assigned_to == '') $unassigned = $asset->assigned_to == '';
|| ($updated_status->deployable == '1') && ($asset->assetstatus?->deployable == '1') $deployable = $updated_status->deployable == '1' && $asset->assetstatus?->deployable == '1';
) { $pending = $updated_status->pending === 1;
if ($unassigned || $deployable || $pending) {
$this->update_array['status_id'] = $updated_status->id; $this->update_array['status_id'] = $updated_status->id;
} }
@@ -423,6 +468,7 @@ class BulkAssetsController extends Controller
} }
/** /**
*
* Start all the custom fields shenanigans * Start all the custom fields shenanigans
*/ */
@@ -430,6 +476,15 @@ class BulkAssetsController extends Controller
if ($asset->model->fieldset) { if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) { foreach ($asset->model->fieldset->fields as $field) {
// null custom fields
if ($custom_fields_to_null) {
foreach ($custom_fields_to_null as $key => $custom_field_to_null) {
if ($field->db_column == $key) {
$this->update_array[$field->db_column] = null;
}
}
}
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) { if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) {
if (Gate::allows('admin')) { if (Gate::allows('admin')) {
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column}); $decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
@@ -562,7 +617,10 @@ class BulkAssetsController extends Controller
public function showCheckout() : View public function showCheckout() : View
{ {
$this->authorize('checkout', Asset::class); $this->authorize('checkout', Asset::class);
return view('hardware/bulk-checkout');
$do_not_change = ['' => trans('general.do_not_change')];
$status_label_list = $do_not_change + Helper::deployableStatusLabelList();
return view('hardware/bulk-checkout')->with('statusLabel_list', $status_label_list);
} }
/** /**
@@ -594,13 +652,13 @@ class BulkAssetsController extends Controller
} }
$checkout_at = date('Y-m-d H:i:s'); $checkout_at = date('Y-m-d H:i:s');
if (($request->filled('checkout_at')) && ($request->get('checkout_at') != date('Y-m-d'))) { if (($request->filled('checkout_at')) && ($request->get('checkout_at') != date('Y-m-d'))) {
$checkout_at = e($request->get('checkout_at')); $checkout_at = $request->get('checkout_at');
} }
$expected_checkin = ''; $expected_checkin = '';
if ($request->filled('expected_checkin')) { if ($request->filled('expected_checkin')) {
$expected_checkin = e($request->get('expected_checkin')); $expected_checkin = $request->get('expected_checkin');
} }
$errors = []; $errors = [];
@@ -608,6 +666,11 @@ class BulkAssetsController extends Controller
foreach ($assets as $asset) { foreach ($assets as $asset) {
$this->authorize('checkout', $asset); $this->authorize('checkout', $asset);
// See if there is a status label passed
if ($request->filled('status_id')) {
$asset->status_id = $request->get('status_id');
}
$checkout_success = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $asset->name, null); $checkout_success = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $asset->name, null);
//TODO - I think this logic is duplicated in the checkOut method? //TODO - I think this logic is duplicated in the checkOut method?
@@ -651,4 +714,25 @@ class BulkAssetsController extends Controller
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success')); return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
} }
} }
public function hasUndeployableStatus (array $asset_ids)
{
$undeployable = Asset::whereIn('id', $asset_ids)
->undeployable()
->get();
$undeployableTags = $undeployable->map(function ($asset) {
return [
'id' => $asset->id,
'asset_tag' => $asset->asset_tag,
];
})->toArray();
$undeployableIds = array_column($undeployableTags, 'id');
$filtered_ids = array_diff($asset_ids, $undeployableIds);
if($undeployable->isNotEmpty()) {
return ['status' => true, 'tags' => $undeployableTags, 'asset_ids' => $filtered_ids];
}
return false;
}
} }

View File

@@ -68,6 +68,7 @@ class CategoriesController extends Controller
$category->eula_text = $request->input('eula_text'); $category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0'); $category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0'); $category->require_acceptance = $request->input('require_acceptance', '0');
$category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0'); $category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes'); $category->notes = $request->input('notes');
$category->created_by = auth()->id(); $category->created_by = auth()->id();
@@ -121,6 +122,7 @@ class CategoriesController extends Controller
$category->eula_text = $request->input('eula_text'); $category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0'); $category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0'); $category->require_acceptance = $request->input('require_acceptance', '0');
$category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0'); $category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes'); $category->notes = $request->input('notes');
@@ -145,7 +147,7 @@ class CategoriesController extends Controller
{ {
$this->authorize('delete', Category::class); $this->authorize('delete', Category::class);
// Check if the category exists // Check if the category exists
if (is_null($category = Category::findOrFail($categoryId))) { if (is_null($category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($categoryId))) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found')); return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found'));
} }
@@ -155,7 +157,6 @@ class CategoriesController extends Controller
Storage::disk('public')->delete('categories'.'/'.$category->image); Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete(); $category->delete();
// Redirect to the locations management page
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success')); return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success'));
} }

View File

@@ -123,11 +123,13 @@ final class CompaniesController extends Controller
*/ */
public function destroy($companyId) : RedirectResponse public function destroy($companyId) : RedirectResponse
{ {
if (is_null($company = Company::find($companyId))) { if (is_null($company = Company::find($companyId))) {
return redirect()->route('companies.index') return redirect()->route('companies.index')
->with('error', trans('admin/companies/message.not_found')); ->with('error', trans('admin/companies/message.not_found'));
} }
$this->authorize('delete', $company); $this->authorize('delete', $company);
if (! $company->isDeletable()) { if (! $company->isDeletable()) {
return redirect()->route('companies.index') return redirect()->route('companies.index')

View File

@@ -100,8 +100,8 @@ class ComponentCheckinController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', return Helper::getRedirectOption($request, $component->id, 'Components')
trans('admin/components/message.checkin.success')); ->with('success', trans('admin/components/message.checkin.success'));
} }
return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist')); return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));

View File

@@ -120,6 +120,7 @@ class ComponentCheckoutController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]); session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.checkout.success')); return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.checkout.success'));
} }
} }

View File

@@ -91,7 +91,8 @@ class ComponentsController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($component->save()) { if ($component->save()) {
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.create.success')); return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.create.success'));
} }
return redirect()->back()->withInput()->withErrors($component->getErrors()); return redirect()->back()->withInput()->withErrors($component->getErrors());
@@ -111,6 +112,7 @@ class ComponentsController extends Controller
{ {
$this->authorize('update', $component); $this->authorize('update', $component);
session()->put('back_url', url()->previous());
return view('components/edit') return view('components/edit')
->with('item', $component) ->with('item', $component)
->with('category_type', 'component'); ->with('category_type', 'component');
@@ -164,7 +166,8 @@ class ComponentsController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($component->save()) { if ($component->save()) {
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.update.success')); return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.update.success'));
} }
return redirect()->back()->withInput()->withErrors($component->getErrors()); return redirect()->back()->withInput()->withErrors($component->getErrors());

View File

@@ -111,6 +111,7 @@ class ConsumableCheckoutController extends Controller
// Redirect to the new consumable page // Redirect to the new consumable page
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.checkout.success')); return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.checkout.success'));
} }
} }

View File

@@ -90,7 +90,8 @@ class ConsumablesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) { if ($consumable->save()) {
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.create.success')); return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.create.success'));
} }
return redirect()->back()->withInput()->withErrors($consumable->getErrors()); return redirect()->back()->withInput()->withErrors($consumable->getErrors());
@@ -107,6 +108,7 @@ class ConsumablesController extends Controller
public function edit(Consumable $consumable) : View | RedirectResponse public function edit(Consumable $consumable) : View | RedirectResponse
{ {
$this->authorize($consumable); $this->authorize($consumable);
session()->put('back_url', url()->previous());
return view('consumables/edit') return view('consumables/edit')
->with('item', $consumable) ->with('item', $consumable)
->with('category_type', 'consumable'); ->with('category_type', 'consumable');
@@ -160,7 +162,8 @@ class ConsumablesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) { if ($consumable->save()) {
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.update.success')); return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.update.success'));
} }
return redirect()->back()->withInput()->withErrors($consumable->getErrors()); return redirect()->back()->withInput()->withErrors($consumable->getErrors());

View File

@@ -86,7 +86,7 @@ class LicenseCheckinController extends Controller
} }
if($licenseSeat->assigned_to != null){ if($licenseSeat->assigned_to != null){
$return_to = User::find($licenseSeat->assigned_to); $return_to = User::withTrashed()->find($licenseSeat->assigned_to);
session()->put('checkedInFrom', $return_to->id); session()->put('checkedInFrom', $return_to->id);
} else { } else {
$return_to = Asset::find($licenseSeat->asset_id); $return_to = Asset::find($licenseSeat->asset_id);
@@ -98,14 +98,17 @@ class LicenseCheckinController extends Controller
$licenseSeat->notes = $request->input('notes'); $licenseSeat->notes = $request->input('notes');
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($request->get('redirect_option') === 'target'){
session()->put(['checkout_to_type' => 'user']);
}
// Was the asset updated? // Was the asset updated?
if ($licenseSeat->save()) { if ($licenseSeat->save()) {
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes'))); event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes')));
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkin.success')); return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.checkin.success'));
} }
// Redirect to the license page with error // Redirect to the license page with error

View File

@@ -89,7 +89,8 @@ class LicenseCheckoutController extends Controller
if ($checkoutTarget) { if ($checkoutTarget) {
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkout.success')); return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.checkout.success'));
} }

View File

@@ -105,7 +105,8 @@ class LicensesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($license->save()) { if ($license->save()) {
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.create.success')); return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.create.success'));
} }
return redirect()->back()->withInput()->withErrors($license->getErrors()); return redirect()->back()->withInput()->withErrors($license->getErrors());
@@ -125,7 +126,7 @@ class LicensesController extends Controller
{ {
$this->authorize('update', $license); $this->authorize('update', $license);
session()->put('back_url', url()->previous());
$maintained_list = [ $maintained_list = [
'' => 'Maintained', '' => 'Maintained',
'1' => 'Yes', '1' => 'Yes',
@@ -181,7 +182,8 @@ class LicensesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($license->save()) { if ($license->save()) {
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.update.success')); return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.update.success'));
} }
// If we can't adjust the number of seats, the error is flashed to the session by the event handler in License.php // If we can't adjust the number of seats, the error is flashed to the session by the event handler in License.php
return redirect()->back()->withInput()->withErrors($license->getErrors()); return redirect()->back()->withInput()->withErrors($license->getErrors());

View File

@@ -3,15 +3,21 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Http\Transformers\ProfileTransformer;
use App\Models\Actionlog;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
use App\Notifications\CurrentInventory; use App\Notifications\CurrentInventory;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View; use \Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/** /**
* This controller handles all actions related to User Profiles for * This controller handles all actions related to User Profiles for
* the Snipe-IT Asset Management application. * the Snipe-IT Asset Management application.
@@ -220,7 +226,7 @@ class ProfileController extends Controller
if (!$user = User::find(auth()->id())) { if (!$user = User::find(auth()->id())) {
return redirect()->back() return redirect()->back()
->with('error', trans('admin/users/message.user_not_found', ['id' => $id])); ->with('error', trans('admin/users/message.user_not_found', ['id' => auth()->id()]));
} }
if (empty($user->email)) { if (empty($user->email)) {
return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email')); return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email'));
@@ -234,4 +240,28 @@ class ProfileController extends Controller
return redirect()->back()->with('success', trans('admin/users/general.user_notified')); return redirect()->back()->with('success', trans('admin/users/general.user_notified'));
} }
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
{
$logentry = Actionlog::where('filename', $filename)->first();
// Make sure the user has permission to view this file
if (auth()->id() != $logentry->target_id) {
return redirect()->route('account')->with('error', trans('general.generic_model_not_found', ['model' => 'file']));
}
if (config('filesystems.default') == 's3_private') {
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
}
if (Storage::exists('private_uploads/eula-pdfs/'.$filename)) {
return response()->download(config('app.private_uploads').'/eula-pdfs/'.$filename);
}
return redirect()->back()->with('error', trans('general.file_does_not_exist'));
}
} }

View File

@@ -184,7 +184,7 @@ class ReportsController extends Controller
$currency = e(Setting::getSettings()->default_currency); $currency = e(Setting::getSettings()->default_currency);
} }
$row[] = $asset->purchase_date; $row[] = Helper::getFormattedDateObject($asset->purchase_date, 'date', false);
$row[] = $currency.Helper::formatCurrencyOutput($asset->purchase_cost); $row[] = $currency.Helper::formatCurrencyOutput($asset->purchase_cost);
$row[] = $currency.Helper::formatCurrencyOutput($asset->getDepreciatedValue()); $row[] = $currency.Helper::formatCurrencyOutput($asset->getDepreciatedValue());
$row[] = $currency.Helper::formatCurrencyOutput(($asset->purchase_cost - $asset->getDepreciatedValue())); $row[] = $currency.Helper::formatCurrencyOutput(($asset->purchase_cost - $asset->getDepreciatedValue()));

View File

@@ -352,6 +352,7 @@ class SettingsController extends Controller
$setting->dash_chart_type = $request->input('dash_chart_type'); $setting->dash_chart_type = $request->input('dash_chart_type');
$setting->profile_edit = $request->input('profile_edit', 0); $setting->profile_edit = $request->input('profile_edit', 0);
$setting->require_checkinout_notes = $request->input('require_checkinout_notes', 0); $setting->require_checkinout_notes = $request->input('require_checkinout_notes', 0);
$setting->manager_view_enabled = $request->input('manager_view_enabled', 0);
if ($request->input('per_page') != '') { if ($request->input('per_page') != '') {
@@ -650,6 +651,7 @@ class SettingsController extends Controller
$setting->alert_email = $alert_email; $setting->alert_email = $alert_email;
$setting->admin_cc_email = $admin_cc_email; $setting->admin_cc_email = $admin_cc_email;
$setting->admin_cc_always = $request->validated('admin_cc_always');
$setting->alerts_enabled = $request->input('alerts_enabled', '0'); $setting->alerts_enabled = $request->input('alerts_enabled', '0');
$setting->alert_interval = $request->input('alert_interval'); $setting->alert_interval = $request->input('alert_interval');
$setting->alert_threshold = $request->input('alert_threshold'); $setting->alert_threshold = $request->input('alert_threshold');
@@ -922,7 +924,7 @@ class SettingsController extends Controller
* @since v5.0.0 * @since v5.0.0
*/ */
public function postSamlSettings(SettingsSamlRequest $request) : RedirectResponse public function postSamlSettings(SettingsSamlRequest $request) : RedirectResponse
{ {
if (is_null($setting = Setting::getSettings())) { if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error')); return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
} }

View File

@@ -154,7 +154,8 @@ class UsersController extends Controller
$user->notify(new WelcomeNotification($data)); $user->notify(new WelcomeNotification($data));
} }
return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users'))->with('success', trans('admin/users/message.success.create')); return Helper::getRedirectOption($request, $user->id, 'Users')
->with('success', trans('admin/users/message.success.create'));
} }
return redirect()->back()->withInput()->withErrors($user->getErrors()); return redirect()->back()->withInput()->withErrors($user->getErrors());
@@ -178,7 +179,7 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0] * @since [v1.0]
* @param $permissions * @param $permissions
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
* @internal param int $id * @internal param int $id
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*/ */
@@ -186,10 +187,15 @@ class UsersController extends Controller
{ {
$this->authorize('update', User::class); $this->authorize('update', User::class);
session()->put('back_url', url()->previous());
$user = User::with(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed()->find($user->id); $user = User::with(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed()->find($user->id);
if ($user) { if ($user) {
if ($user->trashed()) {
return redirect()->route('users.show', $user->id);
}
$permissions = config('permissions'); $permissions = config('permissions');
$groups = Group::pluck('name', 'id'); $groups = Group::pluck('name', 'id');
@@ -308,7 +314,7 @@ class UsersController extends Controller
if ($user->save()) { if ($user->save()) {
// Redirect to the user page // Redirect to the user page
return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users')) return Helper::getRedirectOption($request, $user->id, 'Users')
->with('success', trans('admin/users/message.success.update')); ->with('success', trans('admin/users/message.success.update'));
} }
return redirect()->back()->withInput()->withErrors($user->getErrors()); return redirect()->back()->withInput()->withErrors($user->getErrors());
@@ -506,6 +512,8 @@ class UsersController extends Controller
trans('admin/companies/table.title'), trans('admin/companies/table.title'),
trans('admin/users/table.title'), trans('admin/users/table.title'),
trans('general.employee_number'), trans('general.employee_number'),
trans('admin/users/table.first_name'),
trans('admin/users/table.last_name'),
trans('admin/users/table.name'), trans('admin/users/table.name'),
trans('admin/users/table.username'), trans('admin/users/table.username'),
trans('admin/users/table.email'), trans('admin/users/table.email'),
@@ -551,6 +559,8 @@ class UsersController extends Controller
($user->company) ? $user->company->name : '', ($user->company) ? $user->company->name : '',
$user->jobtitle, $user->jobtitle,
$user->employee_num, $user->employee_num,
$user->first_name,
$user->last_name,
$user->present()->fullName(), $user->present()->fullName(),
$user->username, $user->username,
$user->email, $user->email,

View File

@@ -27,50 +27,126 @@ use Exception;
class ViewAssetsController extends Controller class ViewAssetsController extends Controller
{ {
/** /**
* Redirect to the profile page. * Extract custom fields that should be displayed in user view.
*
* @param User $user
* @return array
*/
private function extractCustomFields(User $user): array
{
$fieldArray = [];
foreach ($user->assets as $asset) {
if ($asset->model && $asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
if ($field->display_in_user_view == '1') {
$fieldArray[$field->db_column] = $field->name;
}
}
}
}
return array_unique($fieldArray);
}
/**
* Get list of users viewable by the current user.
*
* @param User $authUser
* @return \Illuminate\Support\Collection
*/
private function getViewableUsers(User $authUser): \Illuminate\Support\Collection
{
// SuperAdmin sees all users
if ($authUser->isSuperUser()) {
return User::select('id', 'first_name', 'last_name', 'username')
->where('activated', 1)
->orderBy('last_name')
->orderBy('first_name')
->get();
}
// Regular manager sees only their subordinates + self
$managedUsers = $authUser->getAllSubordinates();
// If user has subordinates, show them with self at beginning
if ($managedUsers->count() > 0) {
return collect([$authUser])->merge($managedUsers)
->sortBy('last_name')
->sortBy('first_name');
}
// User has no subordinates, only sees themselves
return collect([$authUser]);
}
/**
* Get the selected user ID from request or default to current user.
*
* @param Request $request
* @param \Illuminate\Support\Collection $subordinates
* @param int $defaultUserId
* @return int
*/
private function getSelectedUserId(Request $request, \Illuminate\Support\Collection $subordinates, int $defaultUserId): int
{
// If no subordinates or no user_id in request, return default
if ($subordinates->count() <= 1 || !$request->filled('user_id')) {
return $defaultUserId;
}
$requestedUserId = (int) $request->input('user_id');
// Validate if the requested user is allowed
if ($subordinates->contains('id', $requestedUserId)) {
return $requestedUserId;
}
// If invalid ID or not authorized, return default
return $defaultUserId;
}
/**
* Show user's assigned assets with optional manager view functionality.
* *
*/ */
public function getIndex() : View | RedirectResponse public function getIndex(Request $request) : View | RedirectResponse
{ {
$user = User::with( $authUser = auth()->user();
$settings = Setting::getSettings();
$subordinates = collect();
$selectedUserId = $authUser->id;
// Process manager view if enabled
if ($settings->manager_view_enabled) {
$subordinates = $this->getViewableUsers($authUser);
$selectedUserId = $this->getSelectedUserId($request, $subordinates, $authUser->id);
}
// Load the data for the user to be viewed (either auth user or selected subordinate)
$userToView = User::with([
'assets', 'assets',
'assets.model', 'assets.model',
'assets.model.fieldset.fields', 'assets.model.fieldset.fields',
'consumables', 'consumables',
'accessories', 'accessories',
'licenses', 'licenses'
)->find(auth()->id()); ])->find($selectedUserId);
$field_array = array();
// Loop through all the custom fields that are applied to any model the user has assigned
foreach ($user->assets as $asset) {
// Make sure the model has a custom fieldset before trying to loop through the associated fields
if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
// check and make sure they're allowed to see the value of the custom field
if ($field->display_in_user_view == '1') {
$field_array[$field->db_column] = $field->name;
}
}
}
// If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error
if (!$userToView) {
return redirect()->route('view-assets')->with('error', trans('admin/users/message.user_not_found'));
} }
// Since some models may re-use the same fieldsets/fields, let's make the array unique so we don't repeat columns // Process custom fields for the user being viewed
array_unique($field_array); $fieldArray = $this->extractCustomFields($userToView);
if (isset($user->id)) { // Pass the necessary data to the view
return view('account/view-assets', compact('user', 'field_array' )) return view('account/view-assets', [
->with('settings', Setting::getSettings()); 'user' => $userToView, // Use 'user' for compatibility with the existing view
} 'field_array' => $fieldArray,
'settings' => $settings,
// Redirect to the user management page 'subordinates' => $subordinates,
return redirect()->route('users.index') 'selectedUserId' => $selectedUserId
->with('error', trans('admin/users/message.user_not_found', $user->id)); ]);
} }
/** /**

View File

@@ -73,6 +73,7 @@ class Kernel extends HttpKernel
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'api-throttle' => \App\Http\Middleware\SetAPIResponseHeaders::class,
'health' => null, 'health' => null,
]; ];
} }

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Symfony\Component\HttpFoundation\Response;
class SetAPIResponseHeaders extends ThrottleRequests
{
/**
* Add the rate limit headers to the response.
*
* This extends the original ThrottleRequests middleware to add the 'X-RateLimit-Reset' and 'Retry-After' headers, even
* if the rate limit is not exceeded.
* @param $maxAttempts
* @param $remainingAttempts
* @param $retryAfter
* @param Response|null $response
* @return array|int[]
*/
protected function getHeaders($maxAttempts, $remainingAttempts, $retryAfter = null, ?Response $response = null)
{
if ($response &&
! is_null($response->headers->get('X-RateLimit-Remaining')) &&
(int) $response->headers->get('X-RateLimit-Remaining') <= (int) $remainingAttempts) {
$headers = [];
$headers['Retry-After'] = $retryAfter; // this is the only line we changed
$headers['X-RateLimit-Reset'] = $retryAfter; // this is the only line we changed
$headers['X-RateLimit-Reset-Timestamp'] = $this->availableAt($retryAfter); // this is the only line we changed
return $headers;
}
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];
if (! is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['X-RateLimit-Reset'] = $retryAfter; // this is the only line we changed
$headers['X-RateLimit-Reset-Timestamp'] = $this->availableAt($retryAfter); // this is the only line we changed
}
return $headers;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
$this->limiter->hit($limit->key, $limit->decaySeconds);
}
$response = $next($request);
foreach ($limits as $limit) {
$response = $this->addHeaders(
$response,
$limit->maxAttempts,
$this->calculateRemainingAttempts($limit->key, $limit->maxAttempts),
$this->getTimeUntilNextRetry($limit->key) // this is the only line we changed
);
}
return $response;
}
}

View File

@@ -41,6 +41,7 @@ class SettingsSamlRequest extends FormRequest
public function withValidator($validator) public function withValidator($validator)
{ {
$validator->after(function ($validator) { $validator->after(function ($validator) {
$setting = Setting::getSettings();
if ($this->input('saml_enabled') == '1') { if ($this->input('saml_enabled') == '1') {
$idpMetadata = $this->input('saml_idp_metadata'); $idpMetadata = $this->input('saml_idp_metadata');
if (! empty($idpMetadata)) { if (! empty($idpMetadata)) {
@@ -56,7 +57,7 @@ class SettingsSamlRequest extends FormRequest
} }
} }
$was_custom_x509cert = strpos(Setting::getSettings()->saml_custom_settings, 'sp_x509cert') !== false; $was_custom_x509cert = strpos($setting->saml_custom_settings, 'sp_x509cert') !== false;
$custom_x509cert = ''; $custom_x509cert = '';
$custom_privateKey = ''; $custom_privateKey = '';
@@ -126,10 +127,14 @@ class SettingsSamlRequest extends FormRequest
} }
if (! (empty($x509cert) && empty($privateKey))) { if (! (empty($x509cert) && empty($privateKey))) {
$this->merge([ // $this->merge([
'saml_sp_x509cert' => $x509cert, // 'saml_sp_x509cert' => $x509cert,
'saml_sp_privatekey' => $privateKey, // 'saml_sp_privatekey' => $privateKey,
]); // ]);
$setting->saml_sp_x509cert = $x509cert;
$setting->saml_sp_privatekey = $privateKey;
$setting->save();
} }
} else { } else {
$validator->errors()->add('saml_integration', 'openssl.cnf is missing/invalid'); $validator->errors()->add('saml_integration', 'openssl.cnf is missing/invalid');
@@ -145,15 +150,21 @@ class SettingsSamlRequest extends FormRequest
} }
if (! empty($x509certNew)) { if (! empty($x509certNew)) {
$this->merge([ // $this->merge([
'saml_sp_x509certNew' => $x509certNew, // 'saml_sp_x509certNew' => $x509certNew,
]); // ]);
$setting->saml_sp_x509certNew = $x509certNew;
$setting->save();
} }
} else { } else {
$this->merge([ // $this->merge([
'saml_sp_x509certNew' => '', // 'saml_sp_x509certNew' => '',
]); // ]);
$setting->saml_sp_x509certNew = '';
$setting->save();
} }
}); });
} }
} }

View File

@@ -37,8 +37,8 @@ class StoreLabelSettings extends FormRequest
return [ return [
'labels_per_page' => 'numeric', 'labels_per_page' => 'numeric',
'labels_width' => 'numeric', 'labels_width' => 'numeric|min:0.1',
'labels_height' => 'numeric', 'labels_height' => 'numeric|min:0.1',
'labels_pmargin_left' => 'numeric|nullable', 'labels_pmargin_left' => 'numeric|nullable',
'labels_pmargin_right' => 'numeric|nullable', 'labels_pmargin_right' => 'numeric|nullable',
'labels_pmargin_top' => 'numeric|nullable', 'labels_pmargin_top' => 'numeric|nullable',

View File

@@ -5,6 +5,7 @@ namespace App\Http\Requests;
use App\Models\Accessory; use App\Models\Accessory;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
class StoreNotificationSettings extends FormRequest class StoreNotificationSettings extends FormRequest
{ {
@@ -26,6 +27,9 @@ class StoreNotificationSettings extends FormRequest
return [ return [
'alert_email' => 'email_array|nullable', 'alert_email' => 'email_array|nullable',
'admin_cc_email' => 'email_array|nullable', 'admin_cc_email' => 'email_array|nullable',
'admin_cc_always' => [
Rule::in('0', '1'),
],
'alert_threshold' => 'numeric|nullable', 'alert_threshold' => 'numeric|nullable',
'alert_interval' => 'numeric|nullable|gt:0', 'alert_interval' => 'numeric|nullable|gt:0',
'audit_warning_days' => 'numeric|nullable', 'audit_warning_days' => 'numeric|nullable',

View File

@@ -6,6 +6,7 @@ use App\Http\Traits\ConvertsBase64ToFiles;
use enshrined\svgSanitize\Sanitizer; use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use \App\Helpers\Helper;
class UploadFileRequest extends Request class UploadFileRequest extends Request
{ {
@@ -27,44 +28,65 @@ class UploadFileRequest extends Request
*/ */
public function rules() public function rules()
{ {
$max_file_size = \App\Helpers\Helper::file_upload_max_size(); $max_file_size = Helper::file_upload_max_size();
return [ return [
'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp,avif|max:'.$max_file_size, 'file.*' => 'required|mimes:'.config('filesystems.allowed_upload_extensions_for_validator').'|max:'.$max_file_size,
]; ];
} }
/** /**
* Sanitizes (if needed) and Saves a file to the appropriate location * Sanitizes (if needed) and Saves a file to the appropriate location
* Returns the 'short' (storage-relative) filename * Returns the 'short' (storage-relative) filename
*
* TODO - this has a lot of similarities to UploadImageRequest's handleImage; is there
* a way to merge them or extend one into the other?
*/ */
public function handleFile(string $dirname, string $name_prefix, $file): string public function handleFile(string $dirname, string $name_prefix, $file): string
{ {
$extension = $file->getClientOriginalExtension(); $extension = $file->getClientOriginalExtension();
$file_name = $name_prefix.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$file->guessExtension(); $file_name = $name_prefix.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$file->guessExtension();
// Check for SVG and sanitize it // Check for SVG and sanitize it
if ($file->getMimeType() === 'image/svg+xml') { if ($file->getMimeType() === 'image/svg+xml') {
Log::debug('This is an SVG'); $uploaded_file = $this->handleSVG($file);
Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put($dirname.$file_name, $cleanSVG);
} catch (\Exception $e) {
Log::debug('Upload no workie :( ');
Log::debug($e);
}
} else { } else {
$put_results = Storage::put($dirname.$file_name, file_get_contents($file)); $uploaded_file = file_get_contents($file);
} }
try {
Storage::put($dirname.$file_name, $uploaded_file);
} catch (\Exception $e) {
Log::debug($e);
}
return $file_name; return $file_name;
} }
}
public function handleSVG($file)
{
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
return $sanitizer->sanitize($dirtySVG);
}
/**
* Get the validation error messages that apply to the request, but
* replace the attribute name with the name of the file that was attempted and failed
* to make it clearer to the user which file is the bad one.
*
* @return array
*/
public function attributes(): array
{
$attributes = [];
if ($this->file) {
for ($i = 0; $i < count($this->file); $i++) {
$attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
}
}
return $attributes;
}
}

View File

@@ -113,8 +113,8 @@ class ActionlogsTransformer
// Display the changes if the user is an admin or superadmin // Display the changes if the user is an admin or superadmin
if (Gate::allows('admin')) { if (Gate::allows('admin')) {
$clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old): ''; $clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old, ['allowed_classes' => false]) : '';
$clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new): ''; $clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new, ['allowed_classes' => false]) : '';
} }
} }
@@ -198,8 +198,8 @@ class ActionlogsTransformer
'note' => ($actionlog->note) ? Helper::parseEscapedMarkedownInline($actionlog->note): null, 'note' => ($actionlog->note) ? Helper::parseEscapedMarkedownInline($actionlog->note): null,
'signature_file' => ($actionlog->accept_signature) ? route('log.signature.view', ['filename' => $actionlog->accept_signature ]) : null, 'signature_file' => ($actionlog->accept_signature) ? route('log.signature.view', ['filename' => $actionlog->accept_signature ]) : null,
'log_meta' => ((isset($clean_meta)) && (is_array($clean_meta))) ? $clean_meta: null, 'log_meta' => ((isset($clean_meta)) && (is_array($clean_meta))) ? $clean_meta: null,
'remote_ip' => ($actionlog->remote_ip) ?? null, 'remote_ip' => e($actionlog->remote_ip) ?? null,
'user_agent' => ($actionlog->user_agent) ?? null, 'user_agent' => e($actionlog->user_agent) ?? null,
'action_source' => ($actionlog->action_source) ?? null, 'action_source' => ($actionlog->action_source) ?? null,
'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'datetime'): Helper::getFormattedDateObject($actionlog->created_at, 'datetime'), 'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'datetime'): Helper::getFormattedDateObject($actionlog->created_at, 'datetime'),
]; ];

View File

@@ -59,7 +59,10 @@ class AssetMaintenancesTransformer
'name'=> e($assetmaintenance->asset->defaultLoc->name), 'name'=> e($assetmaintenance->asset->defaultLoc->name),
] : null, ] : null,
'notes' => ($assetmaintenance->notes) ? Helper::parseEscapedMarkedownInline($assetmaintenance->notes) : null, 'notes' => ($assetmaintenance->notes) ? Helper::parseEscapedMarkedownInline($assetmaintenance->notes) : null,
'supplier' => ($assetmaintenance->supplier) ? ['id' => $assetmaintenance->supplier->id, 'name'=> e($assetmaintenance->supplier->name)] : null, 'supplier' => ($assetmaintenance->supplier) ? [
'id' => $assetmaintenance->supplier->id,
'name'=> e($assetmaintenance->supplier->name)
] : null,
'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost), 'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type), 'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'), 'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),

View File

@@ -80,6 +80,7 @@ class AssetsTransformer
'qr' => ($setting->qr_code=='1') ? config('app.url').'/uploads/barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png' : null, 'qr' => ($setting->qr_code=='1') ? config('app.url').'/uploads/barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png' : null,
'alt_barcode' => ($setting->alt_barcode_enabled=='1') ? config('app.url').'/uploads/barcodes/'.str_slug($setting->alt_barcode).'-'.str_slug($asset->asset_tag).'.png' : null, 'alt_barcode' => ($setting->alt_barcode_enabled=='1') ? config('app.url').'/uploads/barcodes/'.str_slug($setting->alt_barcode).'-'.str_slug($asset->asset_tag).'.png' : null,
'assigned_to' => $this->transformAssignedTo($asset), 'assigned_to' => $this->transformAssignedTo($asset),
'jobtitle' => $asset->assigned ? e($asset->assigned->jobtitle) : null,
'warranty_months' => ($asset->warranty_months > 0) ? e($asset->warranty_months.' '.trans('admin/hardware/form.months')) : null, 'warranty_months' => ($asset->warranty_months > 0) ? e($asset->warranty_months.' '.trans('admin/hardware/form.months')) : null,
'warranty_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null, 'warranty_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null,
'created_by' => ($asset->adminuser) ? [ 'created_by' => ($asset->adminuser) ? [
@@ -101,7 +102,7 @@ class AssetsTransformer
'checkout_counter' => (int) $asset->checkout_counter, 'checkout_counter' => (int) $asset->checkout_counter,
'requests_counter' => (int) $asset->requests_counter, 'requests_counter' => (int) $asset->requests_counter,
'user_can_checkout' => (bool) $asset->availableForCheckout(), 'user_can_checkout' => (bool) $asset->availableForCheckout(),
'book_value' => Helper::formatCurrencyOutput($asset->getLinearDepreciatedValue()), 'book_value' => Helper::formatCurrencyOutput($asset->getDepreciatedValue()),
]; ];

View File

@@ -4,6 +4,10 @@ namespace App\Http\Transformers;
class DatatablesTransformer class DatatablesTransformer
{ {
/**
* Transform data for bootstrap tables and API responses for lists of things
**/
public function transformDatatables($objects, $total = null) public function transformDatatables($objects, $total = null)
{ {
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects); (isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
@@ -11,4 +15,15 @@ class DatatablesTransformer
return $objects_array; return $objects_array;
} }
}
/**
* Transform data for returning the status of items within a bulk action
**/
public function transformBulkResponseWithStatusAndObjects($objects, $total)
{
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
$objects_array['rows'] = $objects;
return $objects_array;
}
}

View File

@@ -26,11 +26,11 @@ class DepreciationsTransformer
$array = [ $array = [
'id' => (int) $depreciation->id, 'id' => (int) $depreciation->id,
'name' => e($depreciation->name), 'name' => e($depreciation->name),
'months' => $depreciation->months.' '.trans('general.months'), 'months' => trans_choice('general.months_plural', $depreciation->months),
'depreciation_min' => $depreciation->depreciation_type === 'percent' ? $depreciation->depreciation_min.'%' : $depreciation->depreciation_min, 'depreciation_min' => $depreciation->depreciation_type === 'percent' ? $depreciation->depreciation_min.'%' : $depreciation->depreciation_min,
'assets_count' => $depreciation->assets_count, 'assets_count' => ($depreciation->assets_count > 0) ? (int) $depreciation->assets_count : 0,
'models_count' => $depreciation->models_count, 'models_count' => ($depreciation->models_count > 0) ? (int) $depreciation->models_count : 0,
'licenses_count' => $depreciation->licenses_count, 'licenses_count' => ($depreciation->licenses_count > 0) ? (int) $depreciation->licenses_count : 0,
'created_by' => ($depreciation->adminuser) ? [ 'created_by' => ($depreciation->adminuser) ? [
'id' => (int) $depreciation->adminuser->id, 'id' => (int) $depreciation->adminuser->id,
'name'=> e($depreciation->adminuser->present()->fullName()), 'name'=> e($depreciation->adminuser->present()->fullName()),

View File

@@ -57,6 +57,10 @@ class LocationsTransformer
'ldap_ou' => ($location->ldap_ou) ? e($location->ldap_ou) : null, 'ldap_ou' => ($location->ldap_ou) ? e($location->ldap_ou) : null,
'notes' => Helper::parseEscapedMarkedownInline($location->notes), 'notes' => Helper::parseEscapedMarkedownInline($location->notes),
'created_at' => Helper::getFormattedDateObject($location->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($location->created_at, 'datetime'),
'created_by' => $location->adminuser ? [
'id' => (int) $location->adminuser->id,
'name'=> e($location->adminuser->present()->fullName),
]: null,
'updated_at' => Helper::getFormattedDateObject($location->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($location->updated_at, 'datetime'),
'parent' => ($location->parent) ? [ 'parent' => ($location->parent) ? [
'id' => (int) $location->parent->id, 'id' => (int) $location->parent->id,

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Database\Eloquent\Collection;
class ProfileTransformer
{
public function transformFiles(Collection $files, $total)
{
$array = [];
foreach ($files as $file) {
$array[] = self::transformFile($file);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformFile(Actionlog $file)
{
$array = [
'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename),
'item' => ($file->item) ? [
'name' => ($file->itemType()=='user') ? e($file->item->getFullNameAttribute()) : e($file->item->getDisplayNameAttribute()),
'type' => e($file->itemType()),
] : null,
'filename' => e($file->filename),
'signature_file' => ($file->accept_signature) ? route('profile.signature.view', ['filename' => $file->accept_signature ]) : null,
'note' => e($file->note),
'url' => route('profile.storedeula.download', ['filename' => $file->filename]),
'file' => route('profile.storedeula.download', ['filename' => $file->filename]),
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
];
return $array;
}
}

View File

@@ -45,6 +45,10 @@ class SuppliersTransformer
'components_count' => (int) $supplier->components_count, 'components_count' => (int) $supplier->components_count,
'notes' => ($supplier->notes) ? Helper::parseEscapedMarkedownInline($supplier->notes) : null, 'notes' => ($supplier->notes) ? Helper::parseEscapedMarkedownInline($supplier->notes) : null,
'created_at' => Helper::getFormattedDateObject($supplier->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($supplier->created_at, 'datetime'),
'created_by' => $supplier->adminuser ? [
'id' => (int) $supplier->adminuser->id,
'name'=> e($supplier->adminuser->present()->fullName),
]: null,
'updated_at' => Helper::getFormattedDateObject($supplier->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($supplier->updated_at, 'datetime'),
]; ];

View File

@@ -3,10 +3,10 @@
namespace App\Http\Transformers; namespace App\Http\Transformers;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class UploadedFilesTransformer class UploadedFilesTransformer
@@ -26,23 +26,26 @@ class UploadedFilesTransformer
{ {
$snipeModel = $file->item_type; $snipeModel = $file->item_type;
// This will be used later as we extend out this transformer to handle more types of uploads
if ($file->item_type == Asset::class) {
$file_url = route('show/assetfile', [$file->item_id, $file->id]);
}
$array = [ $array = [
'id' => (int) $file->id, 'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename),
'name' => e($file->filename),
'item' => ($file->item_type) ? [
'id' => (int) $file->item_id,
'type' => strtolower(class_basename($file->item_type)),
] : null,
'filename' => e($file->filename), 'filename' => e($file->filename),
'url' => $file_url, 'filetype' => StorageHelper::getFiletype($file->uploads_file_path()),
'url' => $file->uploads_file_url(),
'note' => ($file->note) ? e($file->note) : null,
'created_by' => ($file->adminuser) ? [ 'created_by' => ($file->adminuser) ? [
'id' => (int) $file->adminuser->id, 'id' => (int) $file->adminuser->id,
'name'=> e($file->adminuser->present()->fullName), 'name'=> e($file->adminuser->present()->fullName),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($file->updated_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'), 'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'),
'inline' => StorageHelper::allowSafeInline($file->uploads_file_path()),
'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false),
]; ];
$permissions_array['available_actions'] = [ $permissions_array['available_actions'] = [
@@ -53,4 +56,5 @@ class UploadedFilesTransformer
return $array; return $array;
} }
}
}

View File

@@ -50,6 +50,10 @@ class UsersTransformer
'id' => (int) $user->department->id, 'id' => (int) $user->department->id,
'name'=> e($user->department->name), 'name'=> e($user->department->name),
] : null, ] : null,
'department_manager' => ($user->department?->manager) ? [
'id' => (int) $user->department->manager->id,
'name'=> e($user->department->manager->full_name),
] : null,
'location' => ($user->userloc) ? [ 'location' => ($user->userloc) ? [
'id' => (int) $user->userloc->id, 'id' => (int) $user->userloc->id,
'name'=> e($user->userloc->name), 'name'=> e($user->userloc->name),

View File

@@ -80,7 +80,16 @@ class AssetImporter extends ItemImporter
$asset_tag = Asset::autoincrement_asset(); $asset_tag = Asset::autoincrement_asset();
} }
$asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
if ($this->findCsvMatch($row, 'id')!='') {
// Override asset if an ID was given
\Log::debug('Finding asset by ID: '.$this->findCsvMatch($row, 'id'));
$asset = Asset::find($this->findCsvMatch($row, 'id'));
} else {
$asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
}
if ($asset) { if ($asset) {
if (! $this->updating) { if (! $this->updating) {
$exists_error = trans('general.import_asset_tag_exists', ['asset_tag' => $asset_tag]); $exists_error = trans('general.import_asset_tag_exists', ['asset_tag' => $asset_tag]);

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Importer;
use App\Models\Category;
use Illuminate\Support\Facades\Log;
/**
* When we are importing users via an Asset/etc import, we use createOrFetchUser() in
* Importer\Importer.php. [ALG]
*
* Class CategoryImporter
*/
class CategoryImporter extends ItemImporter
{
protected $categories;
public function __construct($filename)
{
parent::__construct($filename);
}
protected function handle($row)
{
parent::handle($row);
$this->createCategoryIfNotExists($row);
}
/**
* Create a category if a duplicate does not exist.
* @todo Investigate how this should interact with Importer::createCategoryIfNotExists
*
* @author A. Gianotto
* @since 6.1.0
* @param array $row
*/
public function createCategoryIfNotExists(array $row)
{
$editingCategory = false;
$category = Category::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
if ($this->findCsvMatch($row, 'id')!='') {
// Override category if an ID was given
\Log::debug('Finding category by ID: '.$this->findCsvMatch($row, 'id'));
$category = Category::find($this->findCsvMatch($row, 'id'));
}
if ($category) {
if (! $this->updating) {
$this->log('A matching Category '.$this->item['name'].' already exists');
return;
}
$this->log('Updating Category');
$editingCategory = true;
} else {
$this->log('No Matching Category, Create a new one');
$category = new Category;
$category->created_by = auth()->id();
}
// Pull the records from the CSV to determine their values
$this->item['name'] = trim($this->findCsvMatch($row, 'name'));
$this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
$this->item['eula_text'] = trim($this->findCsvMatch($row, 'eula_text'));
$this->item['category_type'] = trim(strtolower($this->findCsvMatch($row, 'category_type')));
$this->item['use_default_eula'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'use_default_eula'))) == 1) ? 1 : 0;
$this->item['require_acceptance'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'require_acceptance'))) == 1) ? 1 : 0;
$this->item['checkin_email'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'checkin_email'))) == 1) ? 1 : 0;
Log::debug('Item array is: ');
Log::debug(print_r($this->item, true));
if ($editingCategory) {
Log::debug('Updating existing category');
$category->update($this->sanitizeItemForUpdating($category));
} else {
Log::debug('Creating category');
$category->fill($this->sanitizeItemForStoring($category));
}
if ($category->save()) {
$this->log('Category '.$category->name.' created or updated from CSV import');
return $category;
} else {
Log::debug($category->getErrors());
$this->logError($category, 'Category "'.$this->item['name'].'"');
return $category->errors;
}
}
}

View File

@@ -88,6 +88,7 @@ abstract class Importer
'department' => 'department', 'department' => 'department',
'manager_name' => 'manager full name', 'manager_name' => 'manager full name',
'manager_username' => 'manager username', 'manager_username' => 'manager username',
'manager_employee_num' => 'manager employee number',
'min_amt' => 'minimum quantity', 'min_amt' => 'minimum quantity',
'remote' => 'remote', 'remote' => 'remote',
'vip' => 'vip', 'vip' => 'vip',

View File

@@ -110,7 +110,7 @@ class ItemImporter extends Importer
protected function determineCheckout($row) protected function determineCheckout($row)
{ {
// Locations don't get checked out to anyone/anything // Locations don't get checked out to anyone/anything
if ((get_class($this) == LocationImporter::class) || (get_class($this) == AssetModelImporter::class)) { if ((get_class($this) == LocationImporter::class) || (get_class($this) == AssetModelImporter::class) || (get_class($this) == SupplierImporter::class) || (get_class($this) == ManufacturerImporter::class) || (get_class($this) == CategoryImporter::class)) {
return; return;
} }
@@ -353,16 +353,27 @@ class ItemImporter extends Importer
* @param $user_manager string * @param $user_manager string
* @return int id of company created/found * @return int id of company created/found
*/ */
public function fetchManager($user_manager_first_name, $user_manager_last_name) public function fetchManager($user_manager_username = null, $user_manager_employee_num = null, $user_manager_first_name = null, $user_manager_last_name = null)
{ {
$manager = User::where('first_name', '=', $user_manager_first_name) if ($user_manager_username!='') {
->where('last_name', '=', $user_manager_last_name)->first(); $manager = User::where('username', '=', $user_manager_username)->first();
$this->log('Checking on username '.$user_manager_username);
} elseif ($user_manager_employee_num!='') {
$manager = User::where('employee_num', '=', $user_manager_employee_num)->first();
$this->log('Checking on employee_num '.$user_manager_employee_num);
} else {
$manager = User::where('first_name', '=', $user_manager_first_name)
->where('last_name', '=', $user_manager_last_name)->first();
$this->log('Checking on full name');
}
if ($manager) { if ($manager) {
$this->log('A matching Manager '.$user_manager_first_name.' '.$user_manager_last_name.' already exists'); $this->log('A matching Manager '.$user_manager_first_name.' '.$user_manager_last_name.' already exists');
return $manager->id; return $manager->id;
} }
$this->log('No matching Manager '.$user_manager_first_name.' '.$user_manager_last_name.' found. If their user account is being created through this import, you should re-process this file again. ');
$this->log('No matching Manager found. If their user account is being created through this import, you should re-process this file again. ');
return null; return null;
} }

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Importer;
use App\Models\Manufacturer;
use Illuminate\Support\Facades\Log;
/**
* When we are importing users via an Asset/etc import, we use createOrFetchUser() in
* Importer\Importer.php. [ALG]
*
* Class ManufacturerImporter
*/
class ManufacturerImporter extends ItemImporter
{
protected $manufacturers;
public function __construct($filename)
{
parent::__construct($filename);
}
protected function handle($row)
{
parent::handle($row);
$this->createManufacturerIfNotExists($row);
}
/**
* Create a supplier if a duplicate does not exist.
* @todo Investigate how this should interact with Importer::createManufacturerIfNotExists
*
* @author A. Gianotto
* @since 6.1.0
* @param array $row
*/
public function createManufacturerIfNotExists(array $row)
{
$editingManufacturer = false;
$supplier = Manufacturer::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
if ($this->findCsvMatch($row, 'id')!='') {
// Override supplier if an ID was given
\Log::debug('Finding supplier by ID: '.$this->findCsvMatch($row, 'id'));
$supplier = Manufacturer::find($this->findCsvMatch($row, 'id'));
}
if ($supplier) {
if (! $this->updating) {
$this->log('A matching Manufacturer '.$this->item['name'].' already exists');
return;
}
$this->log('Updating Manufacturer');
$editingManufacturer = true;
} else {
$this->log('No Matching Manufacturer, Create a new one');
$supplier = new Manufacturer;
$supplier->created_by = auth()->id();
}
// Pull the records from the CSV to determine their values
$this->item['name'] = trim($this->findCsvMatch($row, 'name'));
$this->item['support_phone'] = trim($this->findCsvMatch($row, 'support_phone'));
$this->item['fax'] = trim($this->findCsvMatch($row, 'fax'));
$this->item['support_email'] = trim($this->findCsvMatch($row, 'support_email'));
$this->item['contact'] = trim($this->findCsvMatch($row, 'contact'));
$this->item['url'] = trim($this->findCsvMatch($row, 'url'));
$this->item['support_url'] = trim($this->findCsvMatch($row, 'support_url'));
$this->item['warranty_lookup_url'] = trim($this->findCsvMatch($row, 'warranty_lookup_url'));
$this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
Log::debug('Item array is: ');
Log::debug(print_r($this->item, true));
if ($editingManufacturer) {
Log::debug('Updating existing supplier');
$supplier->update($this->sanitizeItemForUpdating($supplier));
} else {
Log::debug('Creating supplier');
$supplier->fill($this->sanitizeItemForStoring($supplier));
}
if ($supplier->save()) {
$this->log('Manufacturer '.$supplier->name.' created or updated from CSV import');
return $supplier;
} else {
Log::debug($supplier->getErrors());
$this->logError($supplier, 'Manufacturer "'.$this->item['name'].'"');
return $supplier->errors;
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Importer;
use App\Models\Supplier;
use Illuminate\Support\Facades\Log;
/**
* When we are importing users via an Asset/etc import, we use createOrFetchUser() in
* Importer\Importer.php. [ALG]
*
* Class SupplierImporter
*/
class SupplierImporter extends ItemImporter
{
protected $suppliers;
public function __construct($filename)
{
parent::__construct($filename);
}
protected function handle($row)
{
parent::handle($row);
$this->createSupplierIfNotExists($row);
}
/**
* Create a supplier if a duplicate does not exist.
* @todo Investigate how this should interact with Importer::createSupplierIfNotExists
*
* @author A. Gianotto
* @since 6.1.0
* @param array $row
*/
public function createSupplierIfNotExists(array $row)
{
$editingSupplier = false;
$supplier = Supplier::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
if ($this->findCsvMatch($row, 'id')!='') {
// Override supplier if an ID was given
\Log::debug('Finding supplier by ID: '.$this->findCsvMatch($row, 'id'));
$supplier = Supplier::find($this->findCsvMatch($row, 'id'));
}
if ($supplier) {
if (! $this->updating) {
$this->log('A matching Supplier '.$this->item['name'].' already exists');
return;
}
$this->log('Updating Supplier');
$editingSupplier = true;
} else {
$this->log('No Matching Supplier, Create a new one');
$supplier = new Supplier;
$supplier->created_by = auth()->id();
}
// Pull the records from the CSV to determine their values
$this->item['name'] = trim($this->findCsvMatch($row, 'name'));
$this->item['address'] = trim($this->findCsvMatch($row, 'address'));
$this->item['address2'] = trim($this->findCsvMatch($row, 'address2'));
$this->item['city'] = trim($this->findCsvMatch($row, 'city'));
$this->item['state'] = trim($this->findCsvMatch($row, 'state'));
$this->item['country'] = trim($this->findCsvMatch($row, 'country'));
$this->item['zip'] = trim($this->findCsvMatch($row, 'zip'));
$this->item['phone'] = trim($this->findCsvMatch($row, 'phone'));
$this->item['fax'] = trim($this->findCsvMatch($row, 'fax'));
$this->item['email'] = trim($this->findCsvMatch($row, 'email'));
$this->item['contact'] = trim($this->findCsvMatch($row, 'contact'));
$this->item['url'] = trim($this->findCsvMatch($row, 'url'));
$this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
Log::debug('Item array is: ');
Log::debug(print_r($this->item, true));
if ($editingSupplier) {
Log::debug('Updating existing supplier');
$supplier->update($this->sanitizeItemForUpdating($supplier));
} else {
Log::debug('Creating supplier');
$supplier->fill($this->sanitizeItemForStoring($supplier));
}
if ($supplier->save()) {
$this->log('Supplier '.$supplier->name.' created or updated from CSV import');
return $supplier;
} else {
Log::debug($supplier->getErrors());
$this->logError($supplier, 'Supplier "'.$this->item['name'].'"');
return $supplier->errors;
}
}
}

View File

@@ -62,7 +62,7 @@ class UserImporter extends ItemImporter
$this->item['activated'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'activated'))) == 1) ? '1' : 0; $this->item['activated'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'activated'))) == 1) ? '1' : 0;
$this->item['employee_num'] = trim($this->findCsvMatch($row, 'employee_num')); $this->item['employee_num'] = trim($this->findCsvMatch($row, 'employee_num'));
$this->item['department_id'] = trim($this->createOrFetchDepartment(trim($this->findCsvMatch($row, 'department')))); $this->item['department_id'] = trim($this->createOrFetchDepartment(trim($this->findCsvMatch($row, 'department'))));
$this->item['manager_id'] = $this->fetchManager(trim($this->findCsvMatch($row, 'manager_first_name')), trim($this->findCsvMatch($row, 'manager_last_name'))); $this->item['manager_id'] = $this->fetchManager(trim($this->findCsvMatch($row, 'manager_username')), trim($this->findCsvMatch($row, 'manager_employee_num')), trim($this->findCsvMatch($row, 'manager_first_name')), trim($this->findCsvMatch($row, 'manager_last_name')));
$this->item['remote'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'remote'))) == 1 ) ? '1' : 0; $this->item['remote'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'remote'))) == 1 ) ? '1' : 0;
$this->item['vip'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'vip'))) ==1 ) ? '1' : 0; $this->item['vip'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'vip'))) ==1 ) ? '1' : 0;
$this->item['autoassign_licenses'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'autoassign_licenses'))) ==1 ) ? '1' : 0; $this->item['autoassign_licenses'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'autoassign_licenses'))) ==1 ) ? '1' : 0;

View File

@@ -12,6 +12,7 @@ use App\Mail\CheckoutConsumableMail;
use App\Mail\CheckoutLicenseMail; use App\Mail\CheckoutLicenseMail;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\Asset; use App\Models\Asset;
use App\Models\Category;
use App\Models\CheckoutAcceptance; use App\Models\CheckoutAcceptance;
use App\Models\Component; use App\Models\Component;
use App\Models\Consumable; use App\Models\Consumable;
@@ -27,6 +28,7 @@ use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CheckoutConsumableNotification; use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseSeatNotification; use App\Notifications\CheckoutLicenseSeatNotification;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
use Exception; use Exception;
@@ -40,6 +42,24 @@ class CheckoutableListener
Component::class, Component::class,
]; ];
/**
* Register the listeners for the subscriber.
*
* @param Illuminate\Events\Dispatcher $events
*/
public function subscribe($events)
{
$events->listen(
\App\Events\CheckoutableCheckedIn::class,
'App\Listeners\CheckoutableListener@onCheckedIn'
);
$events->listen(
\App\Events\CheckoutableCheckedOut::class,
'App\Listeners\CheckoutableListener@onCheckedOut'
);
}
/** /**
* Notify the user and post to webhook about the checked out checkoutable * Notify the user and post to webhook about the checked out checkoutable
* and add a record to the checkout_requests table. * and add a record to the checkout_requests table.
@@ -50,93 +70,70 @@ class CheckoutableListener
return; return;
} }
/**
* Make a checkout acceptance and attach it in the notification
*/
$settings = Setting::getSettings();
$acceptance = $this->getCheckoutAcceptance($event); $acceptance = $this->getCheckoutAcceptance($event);
$adminCcEmailsArray = [];
if ($settings->admin_cc_email !== '') { $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->checkoutable);
$adminCcEmail = $settings->admin_cc_email; $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($acceptance);
$adminCcEmailsArray = array_map('trim', explode(',', $adminCcEmail)); $shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
return;
} }
$ccEmails = array_filter($adminCcEmailsArray);
$mailable = $this->getCheckoutMailType($event, $acceptance);
$notifiable = $this->getNotifiableUsers($event);
if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) {
$mailable = $this->getCheckoutMailType($event, $acceptance);
$notifiable = $this->getNotifiableUser($event);
// Send email notifications $notifiableHasEmail = $notifiable instanceof User && $notifiable->email;
try {
/**
* Send an email if any of the following conditions are met:
* 1. The asset requires acceptance
* 2. The item has a EULA
* 3. The item should send an email at check-in/check-out
* 4. If the admin CC email is set, even if the item being checked out doesn't have an email address (location, etc)
*/
if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() || $shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail;
$this->checkoutableShouldSendEmail($event)) {
[$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
// Send a checkout email to the admin CC addresses, even if the target has no email if (!empty($to)) {
if (!empty($ccEmails)) { try {
Mail::to($ccEmails)->send($mailable); Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable);
Log::info('Checkout Mail sent to CC addresses');
}
// Send a checkout email to the target if it has an email
if (!empty($notifiable->email)) {
Mail::to($notifiable)->send($mailable);
Log::info('Checkout Mail sent to checkout target'); Log::info('Checkout Mail sent to checkout target');
} catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
} }
} }
} catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
} }
// Send notification if ($shouldSendWebhookNotification) {
try { try {
if ($this->shouldSendWebhookNotification()) {
if ($this->newMicrosoftTeamsWebhookEnabled()) { if ($this->newMicrosoftTeamsWebhookEnabled()) {
$message = $this->getCheckoutNotification($event)->toMicrosoftTeams(); $message = $this->getCheckoutNotification($event)->toMicrosoftTeams();
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint); $notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
$notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams $notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams
} else { } else {
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint) Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckoutNotification($event, $acceptance)); ->notify($this->getCheckoutNotification($event, $acceptance));
} }
} catch (ClientException $e) {
if (strpos($e->getMessage(), 'channel_not_found') !== false) {
Log::warning(Setting::getSettings()->webhook_selected . " notification failed: " . $e->getMessage());
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_channel_not_found'));
} else {
Log::error("ClientException caught during checkin notification: " . $e->getMessage());
}
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
} catch (Exception $e) {
Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
'error' => $e->getMessage(),
'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
'event' => $event,
]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
} }
} catch (ClientException $e) {
if (strpos($e->getMessage(), 'channel_not_found') !== false) {
Log::warning(Setting::getSettings()->webhook_selected." notification failed: " . $e->getMessage());
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_channel_not_found') );
}
else {
Log::error("ClientException caught during checkin notification: " . $e->getMessage());
}
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_fail') );
} catch (Exception $e) {
Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
'error' => $e->getMessage(),
'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
'event' => $event,
]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
} }
} }
/** /**
* Notify the user and post to webhook about the checked in checkoutable * Notify the user and post to webhook about the checked in checkoutable
*/ */
public function onCheckedIn($event) public function onCheckedIn($event)
{ {
Log::debug('onCheckedIn in the Checkoutable listener fired'); Log::debug('onCheckedIn in the Checkoutable listener fired');
@@ -145,61 +142,54 @@ class CheckoutableListener
return; return;
} }
/** $shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable);
* Send the appropriate notification $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress();
*/ $shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
if ($event->checkedOutTo && $event->checkoutable){
$acceptances = CheckoutAcceptance::where('checkoutable_id', $event->checkoutable->id)
->where('assigned_to_id', $event->checkedOutTo->id)
->get();
foreach($acceptances as $acceptance){ if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
if($acceptance->isPending()){ return;
$acceptance->delete(); }
if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) {
/**
* Send the appropriate notification
*/
if ($event->checkedOutTo && $event->checkoutable) {
$acceptances = CheckoutAcceptance::where('checkoutable_id', $event->checkoutable->id)
->where('assigned_to_id', $event->checkedOutTo->id)
->get();
foreach ($acceptances as $acceptance) {
if ($acceptance->isPending()) {
$acceptance->delete();
}
} }
} }
}
$settings = Setting::getSettings();
$adminCcEmailsArray = [];
if($settings->admin_cc_email !== '') { $mailable = $this->getCheckinMailType($event);
$adminCcEmail = $settings->admin_cc_email; $notifiable = $this->getNotifiableUser($event);
$adminCcEmailsArray = array_map('trim', explode(',', $adminCcEmail));
}
$ccEmails = array_filter($adminCcEmailsArray);
$mailable = $this->getCheckinMailType($event);
$notifiable = $this->getNotifiableUsers($event);
// Send email notifications $notifiableHasEmail = $notifiable instanceof User && $notifiable->email;
try {
/**
* Send an email if any of the following conditions are met:
* 1. The asset requires acceptance
* 2. The item has a EULA
* 3. The item should send an email at check-in/check-out
* 4. If the admin CC email is set, even if the item being checked in doesn't have an email address (location, etc)
*/
// Send a checkout email to the admin's CC addresses, even if the target has no email $shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail;
if (!empty($ccEmails)) {
Mail::to($ccEmails)->send($mailable); [$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
Log::info('Checkin Mail sent to CC addresses');
try {
if (!empty($to)) {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable);
Log::info('Checkin Mail sent to CC addresses');
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} }
// Send a checkout email to the target if it has an email
if (!empty($notifiable->email)) {
Mail::to($notifiable)->send($mailable);
Log::info('Checkin Mail sent to checkout target');
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} }
// Send Webhook notification if ($shouldSendWebhookNotification) {
try { // Send Webhook notification
if ($this->shouldSendWebhookNotification()) { try {
if ($this->newMicrosoftTeamsWebhookEnabled()) { if ($this->newMicrosoftTeamsWebhookEnabled()) {
$message = $this->getCheckinNotification($event)->toMicrosoftTeams(); $message = $this->getCheckinNotification($event)->toMicrosoftTeams();
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint); $notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
@@ -208,25 +198,24 @@ class CheckoutableListener
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint) Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckinNotification($event)); ->notify($this->getCheckinNotification($event));
} }
} } catch (ClientException $e) {
} catch (ClientException $e) { if (strpos($e->getMessage(), 'channel_not_found') !== false) {
if (strpos($e->getMessage(), 'channel_not_found') !== false) { Log::warning(Setting::getSettings()->webhook_selected . " notification failed: " . $e->getMessage());
Log::warning(Setting::getSettings()->webhook_selected." notification failed: " . $e->getMessage()); return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_channel_not_found'));
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_channel_not_found') ); } else {
} Log::error("ClientException caught during checkin notification: " . $e->getMessage());
else { return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
Log::error("ClientException caught during checkin notification: " . $e->getMessage()); }
} catch (Exception $e) {
Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
'error' => $e->getMessage(),
'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
'event' => $event,
]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail')); return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
} }
} catch (Exception $e) {
Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
'error' => $e->getMessage(),
'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
'event' => $event,
]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_fail'));
} }
} }
/** /**
* Generates a checkout acceptance * Generates a checkout acceptance
@@ -239,6 +228,7 @@ class CheckoutableListener
if ($checkedOutToType != "App\Models\User") { if ($checkedOutToType != "App\Models\User") {
return null; return null;
} }
if (!$event->checkoutable->requireAcceptance()) { if (!$event->checkoutable->requireAcceptance()) {
return null; return null;
} }
@@ -246,15 +236,22 @@ class CheckoutableListener
$acceptance = new CheckoutAcceptance; $acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->checkoutable); $acceptance->checkoutable()->associate($event->checkoutable);
$acceptance->assignedTo()->associate($event->checkedOutTo); $acceptance->assignedTo()->associate($event->checkedOutTo);
$category = $this->getCategoryFromCheckoutable($event->checkoutable);
if ($category?->alert_on_response) {
$acceptance->alert_on_response_id = auth()->id();
}
$acceptance->save(); $acceptance->save();
return $acceptance; return $acceptance;
} }
/** /**
* Get the appropriate notification for the event * Get the appropriate notification for the event
* *
* @param CheckoutableCheckedIn $event * @param CheckoutableCheckedIn $event
* @return Notification * @return Notification
*/ */
private function getCheckinNotification($event) private function getCheckinNotification($event)
@@ -268,7 +265,7 @@ class CheckoutableListener
break; break;
case Asset::class: case Asset::class:
$notificationClass = CheckinAssetNotification::class; $notificationClass = CheckinAssetNotification::class;
break; break;
case LicenseSeat::class: case LicenseSeat::class:
$notificationClass = CheckinLicenseSeatNotification::class; $notificationClass = CheckinLicenseSeatNotification::class;
break; break;
@@ -276,9 +273,8 @@ class CheckoutableListener
Log::debug('Notification class: '.$notificationClass); Log::debug('Notification class: '.$notificationClass);
return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note); return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
} }
/** /**
* Get the appropriate notification for the event * Get the appropriate notification for the event
* *
@@ -320,6 +316,7 @@ class CheckoutableListener
return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note); return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note);
} }
private function getCheckinMailType($event){ private function getCheckinMailType($event){
$lookup = [ $lookup = [
Accessory::class => CheckinAccessoryMail::class, Accessory::class => CheckinAccessoryMail::class,
@@ -341,7 +338,8 @@ class CheckoutableListener
* @param $event * @param $event
* @return mixed * @return mixed
*/ */
private function getNotifiableUsers($event){ private function getNotifiableUser($event)
{
// If it's assigned to an asset, get that asset's assignedTo object // If it's assigned to an asset, get that asset's assignedTo object
if ($event->checkedOutTo instanceof Asset){ if ($event->checkedOutTo instanceof Asset){
@@ -357,6 +355,7 @@ class CheckoutableListener
return $event->checkedOutTo; return $event->checkedOutTo;
} }
} }
private function webhookSelected(){ private function webhookSelected(){
if(Setting::getSettings()->webhook_selected === 'slack' || Setting::getSettings()->webhook_selected === 'general'){ if(Setting::getSettings()->webhook_selected === 'slack' || Setting::getSettings()->webhook_selected === 'general'){
return 'slack'; return 'slack';
@@ -365,60 +364,113 @@ class CheckoutableListener
return Setting::getSettings()->webhook_selected; return Setting::getSettings()->webhook_selected;
} }
/**
* Register the listeners for the subscriber.
*
* @param Illuminate\Events\Dispatcher $events
*/
public function subscribe($events)
{
$events->listen(
\App\Events\CheckoutableCheckedIn::class,
'App\Listeners\CheckoutableListener@onCheckedIn'
);
$events->listen(
\App\Events\CheckoutableCheckedOut::class,
'App\Listeners\CheckoutableListener@onCheckedOut'
);
}
private function shouldNotSendAnyNotifications($checkoutable): bool private function shouldNotSendAnyNotifications($checkoutable): bool
{ {
if(in_array(get_class($checkoutable), $this->skipNotificationsFor)) { return in_array(get_class($checkoutable), $this->skipNotificationsFor);
return true;
}
//runs a check if the category wants to send checkin/checkout emails to users
$category = match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category,
default => null,
};
if (!$category?->checkin_email) {
return true;
}
return false;
} }
private function shouldSendWebhookNotification(): bool private function shouldSendWebhookNotification(): bool
{ {
return Setting::getSettings() && Setting::getSettings()->webhook_endpoint; return Setting::getSettings() && Setting::getSettings()->webhook_endpoint;
} }
private function checkoutableShouldSendEmail($event): bool private function checkoutableCategoryShouldSendEmail(Model $checkoutable): bool
{ {
if($event->checkoutable instanceof LicenseSeat){ if ($checkoutable instanceof LicenseSeat) {
return $event->checkoutable->license->checkin_email(); return $checkoutable->license->checkin_email();
} }
return (method_exists($event->checkoutable, 'checkin_email') && $event->checkoutable->checkin_email()); return (method_exists($checkoutable, 'checkin_email') && $checkoutable->checkin_email());
} }
private function newMicrosoftTeamsWebhookEnabled(): bool private function newMicrosoftTeamsWebhookEnabled(): bool
{ {
return Setting::getSettings()->webhook_selected === 'microsoft' && Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows'); return Setting::getSettings()->webhook_selected === 'microsoft' && Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows');
} }
private function shouldSendCheckoutEmailToUser(Model $checkoutable): bool
{
/**
* Send an email if any of the following conditions are met:
* 1. The asset requires acceptance
* 2. The item has a EULA
* 3. The item should send an email at check-in/check-out
*/
if ($checkoutable->requireAcceptance()) {
return true;
}
if ($checkoutable->getEula()) {
return true;
}
if ($this->checkoutableCategoryShouldSendEmail($checkoutable)) {
return true;
}
return false;
}
private function shouldSendEmailToAlertAddress($acceptance = null): bool
{
$setting = Setting::getSettings();
if (!$setting) {
return false;
}
if (is_null($acceptance) && !$setting->admin_cc_always) {
return false;
}
return (bool) $setting->admin_cc_email;
}
private function getFormattedAlertAddresses(): array
{
$alertAddresses = Setting::getSettings()->admin_cc_email;
if ($alertAddresses !== '') {
return array_filter(array_map('trim', explode(',', $alertAddresses)));
}
return [];
}
private function generateEmailRecipients(
bool $shouldSendEmailToUser,
bool $shouldSendEmailToAlertAddress,
mixed $notifiable
): array {
$to = [];
$cc = [];
// if user && cc: to user, cc admin
if ($shouldSendEmailToUser && $shouldSendEmailToAlertAddress) {
$to[] = $notifiable;
$cc[] = $this->getFormattedAlertAddresses();
}
// if user && no cc: to user
if ($shouldSendEmailToUser && !$shouldSendEmailToAlertAddress) {
$to[] = $notifiable;
}
// if no user && cc: to admin
if (!$shouldSendEmailToUser && $shouldSendEmailToAlertAddress) {
$to[] = $this->getFormattedAlertAddresses();
}
return array($to, $cc);
}
private function getCategoryFromCheckoutable(Model $checkoutable): ?Category
{
return match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category,
};
}
} }

View File

@@ -6,6 +6,8 @@ use Livewire\Component;
class CategoryEditForm extends Component class CategoryEditForm extends Component
{ {
public bool $alertOnResponse;
public $defaultEulaText; public $defaultEulaText;
public $eulaText; public $eulaText;

View File

@@ -35,10 +35,14 @@ class Importer extends Component
public $accessories_fields; public $accessories_fields;
public $assets_fields; public $assets_fields;
public $users_fields; public $users_fields;
public $assetmodels_fields;
public $suppliers_fields;
public $licenses_fields; public $licenses_fields;
public $locations_fields; public $locations_fields;
public $consumables_fields; public $consumables_fields;
public $components_fields; public $components_fields;
public $manufacturers_fields;
public $categories_fields;
public $aliases_fields; public $aliases_fields;
protected $rules = [ protected $rules = [
@@ -85,9 +89,6 @@ class Importer extends Component
case 'component': case 'component':
$results = $this->components_fields; $results = $this->components_fields;
break; break;
case 'consumable':
$results = $this->consumables_fields;
break;
case 'license': case 'license':
$results = $this->licenses_fields; $results = $this->licenses_fields;
break; break;
@@ -97,8 +98,14 @@ class Importer extends Component
case 'location': case 'location':
$results = $this->locations_fields; $results = $this->locations_fields;
break; break;
case 'user': case 'supplier':
$results = $this->users_fields; $results = $this->suppliers_fields;
break;
case 'manufacturer':
$results = $this->manufacturers_fields;
break;
case 'category':
$results = $this->categories_fields;
break; break;
default: default:
$results = []; $results = [];
@@ -128,7 +135,7 @@ class Importer extends Component
//yes, this key *is* valid. Continue on to the next field. //yes, this key *is* valid. Continue on to the next field.
continue; continue;
} else { } else {
//no, this key is *INVALID* for this import type. Better set it to null //no, this key is *INVALID* for this import type. Better set it to null,
// and we'll hope that the $aliases_fields or something else picks it up. // and we'll hope that the $aliases_fields or something else picks it up.
$this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh. $this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh.
} // TODO - strictly speaking, this isn't necessary here I don't think. } // TODO - strictly speaking, this isn't necessary here I don't think.
@@ -149,7 +156,7 @@ class Importer extends Component
// in "Accessories"!) // in "Accessories"!)
if (array_key_exists($key, $this->columnOptions[$type])) { if (array_key_exists($key, $this->columnOptions[$type])) {
$this->field_map[$i] = $key; $this->field_map[$i] = $key;
continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header continue 3; // bust out of both of these loops and the surrounding one - e.g. move on to the next header
} }
} }
} }
@@ -171,6 +178,9 @@ class Importer extends Component
'license' => trans('general.licenses'), 'license' => trans('general.licenses'),
'location' => trans('general.locations'), 'location' => trans('general.locations'),
'user' => trans('general.users'), 'user' => trans('general.users'),
'supplier' => trans('general.suppliers'),
'manufacturer' => trans('general.manufacturers'),
'category' => trans('general.categories'),
]; ];
/** /**
@@ -193,6 +203,7 @@ class Importer extends Component
]; ];
$this->assets_fields = [ $this->assets_fields = [
'id' => trans('general.id'),
'asset_eol_date' => trans('admin/hardware/form.eol_date'), 'asset_eol_date' => trans('admin/hardware/form.eol_date'),
'asset_model' => trans('general.model_name'), 'asset_model' => trans('general.model_name'),
'asset_notes' => trans('general.item_notes', ['item' => trans('admin/hardware/general.asset')]), 'asset_notes' => trans('general.item_notes', ['item' => trans('admin/hardware/general.asset')]),
@@ -319,6 +330,8 @@ class Importer extends Component
'location' => trans('general.location'), 'location' => trans('general.location'),
'manager_first_name' => trans('general.importer.manager_first_name'), 'manager_first_name' => trans('general.importer.manager_first_name'),
'manager_last_name' => trans('general.importer.manager_last_name'), 'manager_last_name' => trans('general.importer.manager_last_name'),
'manager_employee_num' => trans('general.importer.manager_employee_num'),
'manager_username' => trans('general.importer.manager_username'),
'notes' => trans('general.notes'), 'notes' => trans('general.notes'),
'phone_number' => trans('admin/users/table.phone'), 'phone_number' => trans('admin/users/table.phone'),
'remote' => trans('admin/users/general.remote'), 'remote' => trans('admin/users/general.remote'),
@@ -332,6 +345,7 @@ class Importer extends Component
$this->locations_fields = [ $this->locations_fields = [
'id' => trans('general.id'), 'id' => trans('general.id'),
'name' => trans('general.name'),
'address' => trans('general.address'), 'address' => trans('general.address'),
'address2' => trans('general.importer.address2'), 'address2' => trans('general.importer.address2'),
'city' => trans('general.city'), 'city' => trans('general.city'),
@@ -340,13 +354,52 @@ class Importer extends Component
'ldap_ou' => trans('admin/locations/table.ldap_ou'), 'ldap_ou' => trans('admin/locations/table.ldap_ou'),
'manager' => trans('general.importer.manager_full_name'), 'manager' => trans('general.importer.manager_full_name'),
'manager_username' => trans('general.importer.manager_username'), 'manager_username' => trans('general.importer.manager_username'),
'name' => trans('general.item_name_var', ['item' => trans('general.location')]),
'notes' => trans('general.notes'), 'notes' => trans('general.notes'),
'parent_location' => trans('admin/locations/table.parent'), 'parent_location' => trans('admin/locations/table.parent'),
'state' => trans('general.state'), 'state' => trans('general.state'),
'zip' => trans('general.zip'), 'zip' => trans('general.zip'),
]; ];
$this->suppliers_fields = [
'id' => trans('general.id'),
'name' => trans('general.name'),
'address' => trans('general.address'),
'address2' => trans('general.importer.address2'),
'city' => trans('general.city'),
'notes' => trans('general.notes'),
'state' => trans('general.state'),
'zip' => trans('general.zip'),
'phone' => trans('general.phone'),
'fax' => trans('general.fax'),
'url' => trans('general.url'),
'contact' => trans('general.contact'),
'email' => trans('general.email'),
];
$this->manufacturers_fields = [
'id' => trans('general.id'),
'name' => trans('general.name'),
'notes' => trans('general.notes'),
'support_phone' => trans('admin/manufacturers/table.support_phone'),
'support_url' => trans('admin/manufacturers/table.support_url'),
'support_email' => trans('admin/manufacturers/table.support_email'),
'warranty_lookup_url' => trans('admin/manufacturers/table.warranty_lookup_url'),
'url' => trans('general.url'),
];
$this->categories_fields = [
'id' => trans('general.id'),
'name' => trans('general.name'),
'notes' => trans('general.notes'),
'category_type' => trans('admin/categories/general.import_category_type'),
'eula_text' => trans('admin/categories/general.import_eula_text'),
'use_default_eula' => trans('admin/categories/general.use_default_eula_column'),
'require_acceptance' => trans('admin/categories/general.import_require_acceptance'),
'checkin_email' => trans('admin/categories/general.import_checkin_email'),
];
$this->assetmodels_fields = [ $this->assetmodels_fields = [
'category' => trans('general.category'), 'category' => trans('general.category'),
'eol' => trans('general.eol'), 'eol' => trans('general.eol'),
@@ -371,6 +424,8 @@ class Importer extends Component
'consumable name', 'consumable name',
'component name', 'component name',
'name', 'name',
'supplier name',
'location name',
], ],
'item_no' => [ 'item_no' => [
'item number', 'item number',

View File

@@ -10,15 +10,16 @@ class LocationScopeCheck extends Component
{ {
public $mismatched = []; public $mismatched = [];
public $setting; public $setting;
public $is_tested = false;
public function check_locations() public function check_locations()
{ {
$this->mismatched = Helper::test_locations_fmcs(false); $this->mismatched = Helper::test_locations_fmcs(false);
$this->is_tested = true;
} }
public function mount() { public function mount() {
$this->setting = Setting::getSettings(); $this->setting = Setting::getSettings();
$this->mismatched = Helper::test_locations_fmcs(false);
} }
public function render() public function render()

View File

@@ -71,12 +71,12 @@ class SlackSettingsForm extends Component
$this->setting = Setting::getSettings(); $this->setting = Setting::getSettings();
$this->save_button = trans('general.save'); $this->save_button = trans('general.save');
$this->webhook_selected = $this->setting->webhook_selected; $this->webhook_selected = ($this->setting->webhook_selected !== '') ? $this->setting->webhook_selected : 'slack';
$this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"]; $this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"] ?? $this->webhook_text['slack']["name"];
$this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"]; $this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"] ?? $this->webhook_text['slack']["icon"];
$this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"]; $this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"] ?? $this->webhook_text['slack']["placeholder"];
$this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"]; $this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"] ?? $this->webhook_text['slack']["link"];
$this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"]; $this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"] ?? $this->webhook_text['slack']["test"];
$this->webhook_endpoint = $this->setting->webhook_endpoint; $this->webhook_endpoint = $this->setting->webhook_endpoint;
$this->webhook_channel = $this->setting->webhook_channel; $this->webhook_channel = $this->setting->webhook_channel;
$this->webhook_botname = $this->setting->webhook_botname; $this->webhook_botname = $this->setting->webhook_botname;
@@ -90,7 +90,7 @@ class SlackSettingsForm extends Component
$this->isDisabled= ''; $this->isDisabled= '';
} }
if($this->webhook_selected === 'microsoft' && $this->teams_webhook_deprecated) { if($this->webhook_selected === 'microsoft' && $this->teams_webhook_deprecated) {
session()->flash('warning', 'The selected Microsoft Teams webhook URL will be deprecated Jan 31st, 2025. Please use a workflow URL. Microsofts Documentation on creating a workflow can be found <a href="https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498" target="_blank"> here.</a>'); session()->flash('warning', trans('admin/settings/message.webhook.ms_teams_deprecation'));
} }
} }
public function updated($field) { public function updated($field) {

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Mail;
use App\Models\CheckoutAcceptance;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CheckoutAcceptanceResponseMail extends Mailable
{
use Queueable, SerializesModels;
public CheckoutAcceptance $acceptance;
public User $recipient;
public bool $wasAccepted;
/**
* Create a new message instance.
*/
public function __construct(CheckoutAcceptance $acceptance, User $recipient, bool $wasAccepted)
{
$this->acceptance = $acceptance;
$this->recipient = $recipient;
$this->wasAccepted = $wasAccepted;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$subject = $this->wasAccepted
? trans('mail.initiated_accepted')
: trans('mail.initiated_declined');
return new Envelope(
subject: $subject,
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'mail.markdown.checkout-acceptance-response',
with: [
'assignedTo' => $this->acceptance->assignedTo,
'introduction' => $this->introduction(),
'item' => $this->acceptance->checkoutable,
'note' => $this->acceptance->note,
'recipient' => $this->recipient,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
private function introduction(): string
{
return $this->wasAccepted
? trans('mail.following_accepted')
: trans('mail.following_declined');
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Models\Traits\Acceptable; use App\Models\Traits\Acceptable;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable; use App\Models\Traits\Searchable;
use App\Presenters\Presentable; use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -14,7 +15,7 @@ use Watson\Validating\ValidatingTrait;
/** /**
* Model for Accessories. * Model for Accessories.
* *
* @version v1.0 * @version v1.0
*/ */
class Accessory extends SnipeModel class Accessory extends SnipeModel
{ {
@@ -22,6 +23,7 @@ class Accessory extends SnipeModel
protected $presenter = \App\Presenters\AccessoryPresenter::class; protected $presenter = \App\Presenters\AccessoryPresenter::class;
use CompanyableTrait; use CompanyableTrait;
use HasUploads;
use Loggable, Presentable; use Loggable, Presentable;
use SoftDeletes; use SoftDeletes;
@@ -54,8 +56,8 @@ class Accessory extends SnipeModel
]; ];
/** /**
* Accessory validation rules * Accessory validation rules
*/ */
public $rules = [ public $rules = [
'name' => 'required|min:3|max:255', 'name' => 'required|min:3|max:255',
'qty' => 'required|integer|min:1', 'qty' => 'required|integer|min:1',
@@ -69,12 +71,12 @@ class Accessory extends SnipeModel
/** /**
* Whether the model should inject it's identifier to the unique * Whether the model should inject it's identifier to the unique
* validation rules before attempting validation. If this property * validation rules before attempting validation. If this property
* is not set in the model it will default to true. * is not set in the model it will default to true.
* *
* @var bool * @var bool
*/ */
protected $injectUniqueIdentifier = true; protected $injectUniqueIdentifier = true;
use ValidatingTrait; use ValidatingTrait;
@@ -102,29 +104,11 @@ class Accessory extends SnipeModel
]; ];
/**
* Establishes the accessories -> action logs -> uploads relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.1.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
{
return $this->hasMany(\App\Models\Actionlog::class, 'item_id')
->where('item_type', '=', self::class)
->where('action_type', '=', 'uploaded')
->whereNotNull('filename')
->orderBy('created_at', 'desc');
}
/** /**
* Establishes the accessory -> supplier relationship * Establishes the accessory -> supplier relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function supplier() public function supplier()
@@ -137,7 +121,7 @@ class Accessory extends SnipeModel
* Sets the requestable attribute on the accessory * Sets the requestable attribute on the accessory
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
* @return void * @return void
*/ */
public function setRequestableAttribute($value) public function setRequestableAttribute($value)
@@ -152,7 +136,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> company relationship * Establishes the accessory -> company relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function company() public function company()
@@ -164,7 +148,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> location relationship * Establishes the accessory -> location relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function location() public function location()
@@ -176,7 +160,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> category relationship * Establishes the accessory -> category relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function category() public function category()
@@ -188,7 +172,7 @@ class Accessory extends SnipeModel
* Returns the action logs associated with the accessory * Returns the action logs associated with the accessory
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assetlog() public function assetlog()
@@ -217,8 +201,8 @@ class Accessory extends SnipeModel
* *
* It's super-mega-assy, but it's the best I could do for now. * It's super-mega-assy, but it's the best I could do for now.
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since v5.0.0 * @since v5.0.0
* *
* @see \App\Http\Controllers\Api\AccessoriesController\checkedout() * @see \App\Http\Controllers\Api\AccessoriesController\checkedout()
*/ */
@@ -235,7 +219,7 @@ class Accessory extends SnipeModel
* presenter or service provider * presenter or service provider
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return string * @return string
*/ */
public function getImageUrl() public function getImageUrl()
@@ -251,7 +235,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> users relationship * Establishes the accessory -> users relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function checkouts() public function checkouts()
@@ -264,7 +248,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> admin user relationship * Establishes the accessory -> admin user relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v7.0.13] * @since [v7.0.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function adminuser() public function adminuser()
@@ -276,7 +260,7 @@ class Accessory extends SnipeModel
* Checks whether or not the accessory has users * Checks whether or not the accessory has users
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return int * @return int
*/ */
public function hasUsers() public function hasUsers()
@@ -290,7 +274,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> manufacturer relationship * Establishes the accessory -> manufacturer relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function manufacturer() public function manufacturer()
@@ -303,12 +287,12 @@ class Accessory extends SnipeModel
* accessory based on the category it belongs to. * accessory based on the category it belongs to.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return bool * @return bool
*/ */
public function checkin_email() public function checkin_email()
{ {
return $this->category->checkin_email; return $this->category?->checkin_email;
} }
/** /**
@@ -316,7 +300,7 @@ class Accessory extends SnipeModel
* accept it via email. * accept it via email.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return bool * @return bool
*/ */
public function requireAcceptance() public function requireAcceptance()
@@ -329,7 +313,7 @@ class Accessory extends SnipeModel
* checks for a settings level EULA * checks for a settings level EULA
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return string * @return string
*/ */
public function getEula() public function getEula()
@@ -349,7 +333,7 @@ class Accessory extends SnipeModel
* Check how many items within an accessory are checked out * Check how many items within an accessory are checked out
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0] * @since [v5.0]
* @return int * @return int
*/ */
public function numCheckedOut() public function numCheckedOut()
@@ -366,7 +350,7 @@ class Accessory extends SnipeModel
* bad things happen. * bad things happen.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return int * @return int
*/ */
public function numRemaining() public function numRemaining()
@@ -381,8 +365,8 @@ class Accessory extends SnipeModel
/** /**
* Run after the checkout acceptance was declined by the user * Run after the checkout acceptance was declined by the user
* *
* @param User $acceptedBy * @param User $acceptedBy
* @param string $signature * @param string $signature
*/ */
public function declinedCheckout(User $declinedBy, $signature) public function declinedCheckout(User $declinedBy, $signature)
{ {
@@ -408,8 +392,8 @@ class Accessory extends SnipeModel
* This simply checks that there is a value for quantity, and if there isn't, set it to 0. * This simply checks that there is a value for quantity, and if there isn't, set it to 0.
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4 * @since v6.3.4
* @param $value * @param $value
* @return void * @return void
*/ */
public function setQtyAttribute($value) public function setQtyAttribute($value)
@@ -426,7 +410,6 @@ class Accessory extends SnipeModel
/** /**
* Query builder scope to order on created_by name * Query builder scope to order on created_by name
*
*/ */
public function scopeOrderByCreatedByName($query, $order) public function scopeOrderByCreatedByName($query, $order)
{ {
@@ -434,68 +417,68 @@ class Accessory extends SnipeModel
} }
/** /**
* Query builder scope to order on company * Query builder scope to order on company
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
public function scopeOrderCompany($query, $order) public function scopeOrderCompany($query, $order)
{ {
return $query->leftJoin('companies', 'accessories.company_id', '=', 'companies.id') return $query->leftJoin('companies', 'accessories.company_id', '=', 'companies.id')
->orderBy('companies.name', $order); ->orderBy('companies.name', $order);
} }
/** /**
* Query builder scope to order on category * Query builder scope to order on category
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
public function scopeOrderCategory($query, $order) public function scopeOrderCategory($query, $order)
{ {
return $query->leftJoin('categories', 'accessories.category_id', '=', 'categories.id') return $query->leftJoin('categories', 'accessories.category_id', '=', 'categories.id')
->orderBy('categories.name', $order); ->orderBy('categories.name', $order);
} }
/** /**
* Query builder scope to order on location * Query builder scope to order on location
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
public function scopeOrderLocation($query, $order) public function scopeOrderLocation($query, $order)
{ {
return $query->leftJoin('locations', 'accessories.location_id', '=', 'locations.id') return $query->leftJoin('locations', 'accessories.location_id', '=', 'locations.id')
->orderBy('locations.name', $order); ->orderBy('locations.name', $order);
} }
/** /**
* Query builder scope to order on manufacturer * Query builder scope to order on manufacturer
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
public function scopeOrderManufacturer($query, $order) public function scopeOrderManufacturer($query, $order)
{ {
return $query->leftJoin('manufacturers', 'accessories.manufacturer_id', '=', 'manufacturers.id')->orderBy('manufacturers.name', $order); return $query->leftJoin('manufacturers', 'accessories.manufacturer_id', '=', 'manufacturers.id')->orderBy('manufacturers.name', $order);
} }
/** /**
* Query builder scope to order on supplier * Query builder scope to order on supplier
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
public function scopeOrderSupplier($query, $order) public function scopeOrderSupplier($query, $order)
{ {
return $query->leftJoin('suppliers', 'accessories.supplier_id', '=', 'suppliers.id')->orderBy('suppliers.name', $order); return $query->leftJoin('suppliers', 'accessories.supplier_id', '=', 'suppliers.id')->orderBy('suppliers.name', $order);

View File

@@ -16,7 +16,7 @@ use Watson\Validating\ValidatingTrait;
/** /**
* Model for Accessories. * Model for Accessories.
* *
* @version v1.0 * @version v1.0
*/ */
class AccessoryCheckout extends Model class AccessoryCheckout extends Model
{ {
@@ -36,7 +36,7 @@ class AccessoryCheckout extends Model
* Establishes the accessory checkout -> accessory relationship * Establishes the accessory checkout -> accessory relationship
* *
* @author [A. Kroeger] * @author [A. Kroeger]
* @since [v7.0.9] * @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function accessory() public function accessory()
@@ -52,7 +52,7 @@ class AccessoryCheckout extends Model
* Establishes the accessory checkout -> user relationship * Establishes the accessory checkout -> user relationship
* *
* @author [A. Kroeger] * @author [A. Kroeger]
* @since [v7.0.9] * @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function adminuser() public function adminuser()
@@ -64,7 +64,7 @@ class AccessoryCheckout extends Model
* Get the target this asset is checked out to * Get the target this asset is checked out to
* *
* @author [A. Kroeger] * @author [A. Kroeger]
* @since [v7.0] * @since [v7.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assignedTo() public function assignedTo()
@@ -76,7 +76,7 @@ class AccessoryCheckout extends Model
* Gets the lowercased name of the type of target the asset is assigned to * Gets the lowercased name of the type of target the asset is assigned to
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
* @return string * @return string
*/ */
public function assignedType() public function assignedType()
@@ -91,7 +91,7 @@ class AccessoryCheckout extends Model
* this method is an easy way of seeing if we are checked out to a user. * this method is an easy way of seeing if we are checked out to a user.
* *
* @author [A. Kroeger] * @author [A. Kroeger]
* @since [v7.0] * @since [v7.0]
*/ */
public function checkedOutToUser(): bool public function checkedOutToUser(): bool
{ {
@@ -127,50 +127,64 @@ class AccessoryCheckout extends Model
* Run additional, advanced searches. * Run additional, advanced searches.
* *
* @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param array $terms The search terms * @param array $terms The search terms
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
public function advancedTextSearch(Builder $query, array $terms) public function advancedTextSearch(Builder $query, array $terms)
{ {
$userQuery = User::where(function ($query) use ($terms) { $userQuery = User::where(
foreach ($terms as $term) { function ($query) use ($terms) {
$search_str = '%' . $term . '%'; foreach ($terms as $term) {
$query->where('first_name', 'like', $search_str) $search_str = '%' . $term . '%';
->orWhere('last_name', 'like', $search_str) $query->where('first_name', 'like', $search_str)
->orWhere('note', 'like', $search_str); ->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
}
} }
})->select('id'); )->select('id');
$locationQuery = Location::where(function ($query) use ($terms) { $locationQuery = Location::where(
foreach ($terms as $term) { function ($query) use ($terms) {
$search_str = '%' . $term . '%'; foreach ($terms as $term) {
$query->where('name', 'like', $search_str); $search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
} }
})->select('id'); )->select('id');
$assetQuery = Asset::where(function ($query) use ($terms) { $assetQuery = Asset::where(
foreach ($terms as $term) { function ($query) use ($terms) {
$search_str = '%' . $term . '%'; foreach ($terms as $term) {
$query->where('name', 'like', $search_str); $search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
} }
})->select('id'); )->select('id');
$query->where(function ($query) use ($userQuery) { $query->where(
$query->where('assigned_type', User::class) function ($query) use ($userQuery) {
->whereIn('assigned_to', $userQuery); $query->where('assigned_type', User::class)
})->orWhere(function($query) use ($locationQuery) { ->whereIn('assigned_to', $userQuery);
$query->where('assigned_type', Location::class)
->whereIn('assigned_to', $locationQuery);
})->orWhere(function($query) use ($assetQuery) {
$query->where('assigned_type', Asset::class)
->whereIn('assigned_to', $assetQuery);
})->orWhere(function($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('note', 'like', $search_str);
} }
}); )->orWhere(
function ($query) use ($locationQuery) {
$query->where('assigned_type', Location::class)
->whereIn('assigned_to', $locationQuery);
}
)->orWhere(
function ($query) use ($assetQuery) {
$query->where('assigned_type', Asset::class)
->whereIn('assigned_to', $assetQuery);
}
)->orWhere(
function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('note', 'like', $search_str);
}
}
);
return $query; return $query;
} }

View File

@@ -12,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* Model for the Actionlog (the table that keeps a historical log of * Model for the Actionlog (the table that keeps a historical log of
* checkouts, checkins, and updates). * checkouts, checkins, and updates).
* *
* @version v1.0 * @version v1.0
*/ */
class Actionlog extends SnipeModel class Actionlog extends SnipeModel
{ {
@@ -69,8 +69,8 @@ class Actionlog extends SnipeModel
*/ */
protected $searchableRelations = [ protected $searchableRelations = [
'company' => ['name'], 'company' => ['name'],
'adminuser' => ['first_name','last_name','username', 'email'], 'adminuser' => ['first_name','last_name','username', 'email', 'employee_num'],
'user' => ['first_name','last_name','username', 'email'], 'user' => ['first_name','last_name','username', 'email', 'employee_num'],
'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'], 'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'],
'assets.model' => ['name', 'model_number', 'eol', 'notes'], 'assets.model' => ['name', 'model_number', 'eol', 'notes'],
'assets.model.category' => ['name', 'notes'], 'assets.model.category' => ['name', 'notes'],
@@ -96,24 +96,31 @@ class Actionlog extends SnipeModel
* Override from Builder to automatically add the company * Override from Builder to automatically add the company
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public static function boot() public static function boot()
{ {
parent::boot(); parent::boot();
static::creating(function (self $actionlog) { static::creating(
// If the admin is a superadmin, let's see if the target instead has a company. function (self $actionlog) {
if (auth()->user() && auth()->user()->isSuperUser()) { // If the admin is a superadmin, let's see if the target instead has a company.
if ($actionlog->target) { if (auth()->user() && auth()->user()->isSuperUser()) {
$actionlog->company_id = $actionlog->target->company_id; if ($actionlog->target) {
} elseif ($actionlog->item) { $actionlog->company_id = $actionlog->target->company_id;
$actionlog->company_id = $actionlog->item->company_id; } elseif ($actionlog->item) {
$actionlog->company_id = $actionlog->item->company_id;
}
} elseif (auth()->user() && auth()->user()->company) {
$actionlog->company_id = auth()->user()->company_id;
} }
} elseif (auth()->user() && auth()->user()->company) {
$actionlog->company_id = auth()->user()->company_id; if ($actionlog->action_date == '') {
$actionlog->action_date = Carbon::now();
}
} }
}); );
} }
@@ -121,7 +128,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> item relationship * Establishes the actionlog -> item relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function item() public function item()
@@ -133,7 +140,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> company relationship * Establishes the actionlog -> company relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function company() public function company()
@@ -146,7 +153,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> asset relationship * Establishes the actionlog -> asset relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assets() public function assets()
@@ -158,7 +165,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> license relationship * Establishes the actionlog -> license relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function licenses() public function licenses()
@@ -170,7 +177,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> consumable relationship * Establishes the actionlog -> consumable relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function consumables() public function consumables()
@@ -182,7 +189,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> consumable relationship * Establishes the actionlog -> consumable relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function accessories() public function accessories()
@@ -194,7 +201,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> components relationship * Establishes the actionlog -> components relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function components() public function components()
@@ -206,7 +213,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> item type relationship * Establishes the actionlog -> item type relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function itemType() public function itemType()
@@ -222,7 +229,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> target type relationship * Establishes the actionlog -> target type relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function targetType() public function targetType()
@@ -239,21 +246,21 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> uploads relationship * Establishes the actionlog -> uploads relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function uploads() public function uploads()
{ {
return $this->morphTo('item') return $this->morphTo('item')
->where('action_type', '=', 'uploaded') ->where('action_type', '=', 'uploaded')
->withTrashed(); ->withTrashed();
} }
/** /**
* Establishes the actionlog -> userlog relationship * Establishes the actionlog -> userlog relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function userlog() public function userlog()
@@ -265,20 +272,20 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> admin user relationship * Establishes the actionlog -> admin user relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function adminuser() public function adminuser()
{ {
return $this->belongsTo(User::class, 'created_by') return $this->belongsTo(User::class, 'created_by')
->withTrashed(); ->withTrashed();
} }
/** /**
* Establishes the actionlog -> user relationship * Establishes the actionlog -> user relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function user() public function user()
@@ -291,7 +298,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> target relationship * Establishes the actionlog -> target relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function target() public function target()
@@ -303,7 +310,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> location relationship * Establishes the actionlog -> location relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function location() public function location()
@@ -316,7 +323,7 @@ class Actionlog extends SnipeModel
* Check if the file exists, and if it does, force a download * Check if the file exists, and if it does, force a download
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return string | false * @return string | false
*/ */
public function get_src($type = 'assets', $fieldname = 'filename') public function get_src($type = 'assets', $fieldname = 'filename')
@@ -334,7 +341,7 @@ class Actionlog extends SnipeModel
* Saves the log record with the action type * Saves the log record with the action type
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return bool * @return bool
*/ */
public function logaction($actiontype) public function logaction($actiontype)
@@ -355,7 +362,7 @@ class Actionlog extends SnipeModel
* Calculate the number of days until the next audit * Calculate the number of days until the next audit
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
* @return int * @return int
*/ */
public function daysUntilNextAudit($monthInterval = 12, $asset = null) public function daysUntilNextAudit($monthInterval = 12, $asset = null)
@@ -376,7 +383,7 @@ class Actionlog extends SnipeModel
if ($this->created_at > $override_default_next) { if ($this->created_at > $override_default_next) {
$next_audit_days = '-'.$next_audit_days; $next_audit_days = '-'.$next_audit_days;
} }
return $next_audit_days; return $next_audit_days;
} }
@@ -384,7 +391,7 @@ class Actionlog extends SnipeModel
* Calculate the date of the next audit * Calculate the date of the next audit
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
* @return \Datetime * @return \Datetime
*/ */
public function calcNextAuditDate($monthInterval = 12, $asset = null) public function calcNextAuditDate($monthInterval = 12, $asset = null)
@@ -401,24 +408,24 @@ class Actionlog extends SnipeModel
/** /**
* Gets action logs in chronological order, excluding uploads * Gets action logs in chronological order, excluding uploads
* *
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @since v1.0 * @since v1.0
* @return \Illuminate\Database\Eloquent\Collection * @return \Illuminate\Database\Eloquent\Collection
*/ */
public function getListingOfActionLogsChronologicalOrder() public function getListingOfActionLogsChronologicalOrder()
{ {
return $this->all() return $this->all()
->where('action_type', '!=', 'uploaded') ->where('action_type', '!=', 'uploaded')
->orderBy('item_id', 'asc') ->orderBy('item_id', 'asc')
->orderBy('created_at', 'asc') ->orderBy('created_at', 'asc')
->get(); ->get();
} }
/** /**
* Determines what the type of request is so we can log it to the action_log * Determines what the type of request is so we can log it to the action_log
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since v6.3.0 * @since v6.3.0
* @return string * @return string
*/ */
public function determineActionSource(): string public function determineActionSource(): string
@@ -430,11 +437,12 @@ class Actionlog extends SnipeModel
// This is an API call // This is an API call
if (((request()->header('content-type') && (request()->header('accept'))=='application/json')) if (((request()->header('content-type') && (request()->header('accept'))=='application/json'))
&& (starts_with(request()->header('authorization'), 'Bearer '))) { && (starts_with(request()->header('authorization'), 'Bearer '))
) {
return 'api'; return 'api';
} }
// This is probably NOT an API call // This is probably NOT an API call
if (request()->filled('_token')) { if (request()->filled('_token')) {
return 'gui'; return 'gui';
} }
@@ -444,6 +452,62 @@ class Actionlog extends SnipeModel
} }
public function uploads_file_url()
{
switch ($this->item_type) {
case Accessory::class:
return route('show.accessoryfile', [$this->item_id, $this->id]);
case Asset::class:
return route('show/assetfile', [$this->item_id, $this->id]);
case AssetModel::class:
return route('show/modelfile', [$this->item_id, $this->id]);
case Consumable::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case Component::class:
return route('show.componentfile', [$this->item_id, $this->id]);
case License::class:
return route('show.licensefile', [$this->item_id, $this->id]);
case Location::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case User::class:
return route('show/userfile', [$this->item_id, $this->id]);
default:
return null;
}
}
public function uploads_file_path()
{
switch ($this->item_type) {
case Accessory::class:
return 'private_uploads/accessories/'.$this->filename;
case Asset::class:
return 'private_uploads/assets/'.$this->filename;
case AssetModel::class:
return 'private_uploads/assetmodels/'.$this->filename;
case Consumable::class:
return 'private_uploads/consumables/'.$this->filename;
case Component::class:
return 'private_uploads/components/'.$this->filename;
case License::class:
return 'private_uploads/licenses/'.$this->filename;
case Location::class:
return 'private_uploads/locations/'.$this->filename;
case User::class:
return 'private_uploads/users/'.$this->filename;
default:
return null;
}
}
// Manually sets $this->source for determineActionSource() // Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void public function setActionSource($source = null): void
{ {
@@ -454,4 +518,4 @@ class Actionlog extends SnipeModel
{ {
return $query->leftJoin('users as admin_sort', 'action_logs.created_by', '=', 'admin_sort.id')->select('action_logs.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order); return $query->leftJoin('users as admin_sort', 'action_logs.created_by', '=', 'admin_sort.id')->select('action_logs.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ use Watson\Validating\ValidatingTrait;
/** /**
* Model for Asset Maintenances. * Model for Asset Maintenances.
* *
* @version v1.0 * @version v1.0
*/ */
class AssetMaintenance extends Model implements ICompanyableChild class AssetMaintenance extends Model implements ICompanyableChild
{ {
@@ -26,12 +26,12 @@ class AssetMaintenance extends Model implements ICompanyableChild
protected $table = 'asset_maintenances'; protected $table = 'asset_maintenances';
protected $rules = [ protected $rules = [
'asset_id' => 'required|integer', 'asset_id' => 'required|integer',
'supplier_id' => 'required|integer', 'supplier_id' => 'nullable|integer',
'asset_maintenance_type' => 'required', 'asset_maintenance_type' => 'required',
'title' => 'required|max:100', 'title' => 'required|max:100',
'is_warranty' => 'boolean', 'is_warranty' => 'boolean',
'start_date' => 'required|date_format:Y-m-d', 'start_date' => 'required|date_format:Y-m-d',
'completion_date' => 'date_format:Y-m-d|nullable', 'completion_date' => 'date_format:Y-m-d|nullable|after_or_equal:start_date',
'notes' => 'string|nullable', 'notes' => 'string|nullable',
'cost' => 'numeric|nullable', 'cost' => 'numeric|nullable',
]; ];
@@ -93,7 +93,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
/** /**
* getImprovementOptions * getImprovementOptions
* *
* @return array * @return array
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0 * @version v1.0
*/ */
@@ -157,20 +157,21 @@ class AssetMaintenance extends Model implements ICompanyableChild
* asset * asset
* Get asset for this improvement * Get asset for this improvement
* *
* @return mixed * @return mixed
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0 * @version v1.0
*/ */
public function asset() public function asset()
{ {
return $this->belongsTo(\App\Models\Asset::class, 'asset_id') return $this->belongsTo(\App\Models\Asset::class, 'asset_id')
->withTrashed(); ->withTrashed();
} }
/** /**
* Get the admin who created the maintenance * Get the admin who created the maintenance
* *
* @return mixed * @return mixed
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @version v3.0 * @version v3.0
*/ */
@@ -183,7 +184,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
public function supplier() public function supplier()
{ {
return $this->belongsTo(\App\Models\Supplier::class, 'supplier_id') return $this->belongsTo(\App\Models\Supplier::class, 'supplier_id')
->withTrashed(); ->withTrashed();
} }
/** /**
@@ -195,8 +196,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/** /**
* Query builder scope to order on a supplier * Query builder scope to order on a supplier
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -211,8 +212,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/** /**
* Query builder scope to order on asset tag * Query builder scope to order on asset tag
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -225,8 +226,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/** /**
* Query builder scope to order on asset tag * Query builder scope to order on asset tag
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -239,8 +240,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/** /**
* Query builder scope to order on serial * Query builder scope to order on serial
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -253,8 +254,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
/** /**
* Query builder scope to order on status label name * Query builder scope to order on status label name
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -265,6 +266,21 @@ class AssetMaintenance extends Model implements ICompanyableChild
->orderBy('maintained_asset_status.name', $order); ->orderBy('maintained_asset_status.name', $order);
} }
/**
* Query builder scope to order on status label name
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderLocationName($query, $order)
{
return $query->join('assets as maintained_asset', 'asset_maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('locations as maintained_asset_location', 'maintained_asset_location.id', '=', 'maintained_asset.location_id')
->orderBy('maintained_asset_location.name', $order);
}
/** /**
* Query builder scope to order on the user that created it * Query builder scope to order on the user that created it
*/ */

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable; use App\Models\Traits\Searchable;
use App\Presenters\Presentable; use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -16,7 +17,7 @@ use App\Http\Traits\TwoColumnUniqueUndeletedTrait;
* Model for Asset Models. Asset Models contain higher level * Model for Asset Models. Asset Models contain higher level
* attributes that are common among the same type of asset. * attributes that are common among the same type of asset.
* *
* @version v1.0 * @version v1.0
*/ */
class AssetModel extends SnipeModel class AssetModel extends SnipeModel
{ {
@@ -24,6 +25,7 @@ class AssetModel extends SnipeModel
use SoftDeletes; use SoftDeletes;
use Loggable, Requestable, Presentable; use Loggable, Requestable, Presentable;
use TwoColumnUniqueUndeletedTrait; use TwoColumnUniqueUndeletedTrait;
use HasUploads;
/** /**
* Whether the model should inject its identifier to the unique * Whether the model should inject its identifier to the unique
@@ -103,7 +105,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> assets relationship * Establishes the model -> assets relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0] * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assets() public function assets()
@@ -115,7 +117,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> category relationship * Establishes the model -> category relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0] * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function category() public function category()
@@ -127,7 +129,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> depreciation relationship * Establishes the model -> depreciation relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0] * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function depreciation() public function depreciation()
@@ -139,7 +141,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> manufacturer relationship * Establishes the model -> manufacturer relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0] * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function manufacturer() public function manufacturer()
@@ -151,7 +153,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> fieldset relationship * Establishes the model -> fieldset relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0] * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function fieldset() public function fieldset()
@@ -161,14 +163,14 @@ class AssetModel extends SnipeModel
public function customFields() public function customFields()
{ {
return $this->fieldset()->first()->fields(); return $this->fieldset()->first()->fields();
} }
/** /**
* Establishes the model -> custom field default values relationship * Establishes the model -> custom field default values relationship
* *
* @author hannah tinkler * @author hannah tinkler
* @since [v4.3] * @since [v4.3]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function defaultValues() public function defaultValues()
@@ -182,7 +184,7 @@ class AssetModel extends SnipeModel
* @todo this should probably be moved * @todo this should probably be moved
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0] * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function getImageUrl() public function getImageUrl()
@@ -199,7 +201,7 @@ class AssetModel extends SnipeModel
* Checks if the model is deletable * Checks if the model is deletable
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v6.3.4] * @since [v6.3.4]
* @return bool * @return bool
*/ */
public function isDeletable() public function isDeletable()
@@ -209,27 +211,12 @@ class AssetModel extends SnipeModel
&& ($this->deleted_at == ''); && ($this->deleted_at == '');
} }
/**
* Get uploads for this model
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
{
return $this->hasMany('\App\Models\Actionlog', 'item_id')
->where('item_type', '=', AssetModel::class)
->where('action_type', '=', 'uploaded')
->whereNotNull('filename')
->orderBy('created_at', 'desc');
}
/** /**
* Get user who created the item * Get user who created the item
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0] * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function adminuser() public function adminuser()
@@ -248,10 +235,10 @@ class AssetModel extends SnipeModel
* scopeInCategory * scopeInCategory
* Get all models that are in the array of category ids * Get all models that are in the array of category ids
* *
* @param $query * @param $query
* @param array $categoryIdListing * @param array $categoryIdListing
* *
* @return mixed * @return mixed
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0 * @version v1.0
*/ */
@@ -264,9 +251,9 @@ class AssetModel extends SnipeModel
* scopeRequestable * scopeRequestable
* Get all models that are requestable by a user. * Get all models that are requestable by a user.
* *
* @param $query * @param $query
* *
* @return $query * @return $query
* @author Daniel Meltzer <dmeltzer.devel@gmail.com> * @author Daniel Meltzer <dmeltzer.devel@gmail.com>
* @version v3.5 * @version v3.5
*/ */
@@ -278,8 +265,8 @@ class AssetModel extends SnipeModel
/** /**
* Query builder scope to search on text, including catgeory and manufacturer name * Query builder scope to search on text, including catgeory and manufacturer name
* *
* @param Illuminate\Database\Query\Builder $query Query builder instance * @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term * @param text $search Search term
* *
* @return Illuminate\Database\Query\Builder Modified query builder * @return Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -287,23 +274,31 @@ class AssetModel extends SnipeModel
{ {
return $query->where('models.name', 'LIKE', "%$search%") return $query->where('models.name', 'LIKE', "%$search%")
->orWhere('model_number', 'LIKE', "%$search%") ->orWhere('model_number', 'LIKE', "%$search%")
->orWhere(function ($query) use ($search) { ->orWhere(
$query->whereHas('category', function ($query) use ($search) { function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%'); $query->whereHas(
}); 'category', function ($query) use ($search) {
}) $query->where('categories.name', 'LIKE', '%'.$search.'%');
->orWhere(function ($query) use ($search) { }
$query->whereHas('manufacturer', function ($query) use ($search) { );
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%'); }
}); )
}); ->orWhere(
function ($query) use ($search) {
$query->whereHas(
'manufacturer', function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
}
);
}
);
} }
/** /**
* Query builder scope to order on manufacturer * Query builder scope to order on manufacturer
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -315,8 +310,8 @@ class AssetModel extends SnipeModel
/** /**
* Query builder scope to order on category name * Query builder scope to order on category name
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -332,7 +327,6 @@ class AssetModel extends SnipeModel
/** /**
* Query builder scope to order on created_by name * Query builder scope to order on created_by name
*
*/ */
public function scopeOrderByCreatedByName($query, $order) public function scopeOrderByCreatedByName($query, $order)
{ {

View File

@@ -18,7 +18,7 @@ use Illuminate\Support\Str;
* to require acceptance from the user, whether or not to * to require acceptance from the user, whether or not to
* send a EULA to the user, etc. * send a EULA to the user, etc.
* *
* @version v1.0 * @version v1.0
*/ */
class Category extends SnipeModel class Category extends SnipeModel
{ {
@@ -32,6 +32,7 @@ class Category extends SnipeModel
protected $hidden = ['created_by', 'deleted_at']; protected $hidden = ['created_by', 'deleted_at'];
protected $casts = [ protected $casts = [
'alert_on_response' => 'boolean',
'created_by' => 'integer', 'created_by' => 'integer',
]; ];
@@ -69,6 +70,7 @@ class Category extends SnipeModel
'eula_text', 'eula_text',
'name', 'name',
'require_acceptance', 'require_acceptance',
'alert_on_response',
'use_default_eula', 'use_default_eula',
'created_by', 'created_by',
'notes', 'notes',
@@ -94,12 +96,20 @@ class Category extends SnipeModel
* Checks if category can be deleted * Checks if category can be deleted
* *
* @author [Dan Meltzer] [<dmeltzer.devel@gmail.com>] * @author [Dan Meltzer] [<dmeltzer.devel@gmail.com>]
* @since [v5.0] * @since [v5.0]
* @return bool * @return bool
*/ */
public function isDeletable() public function isDeletable()
{ {
// We have to check for models as well if the category type is asset
if ($this->category_type == 'asset') {
return Gate::allows('delete', $this)
&& ($this->itemCount() == 0)
&& ($this->models_count == 0)
&& ($this->deleted_at == '');
}
return Gate::allows('delete', $this) return Gate::allows('delete', $this)
&& ($this->itemCount() == 0) && ($this->itemCount() == 0)
&& ($this->deleted_at == ''); && ($this->deleted_at == '');
@@ -109,7 +119,7 @@ class Category extends SnipeModel
* Establishes the category -> accessories relationship * Establishes the category -> accessories relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0] * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function accessories() public function accessories()
@@ -121,7 +131,7 @@ class Category extends SnipeModel
* Establishes the category -> licenses relationship * Establishes the category -> licenses relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.3] * @since [v4.3]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function licenses() public function licenses()
@@ -133,7 +143,7 @@ class Category extends SnipeModel
* Establishes the category -> consumables relationship * Establishes the category -> consumables relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function consumables() public function consumables()
@@ -145,7 +155,7 @@ class Category extends SnipeModel
* Establishes the category -> consumables relationship * Establishes the category -> consumables relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function components() public function components()
@@ -160,7 +170,7 @@ class Category extends SnipeModel
* It should only be used in a single category context. * It should only be used in a single category context.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0] * @since [v2.0]
* @return int * @return int
*/ */
public function itemCount() public function itemCount()
@@ -171,18 +181,18 @@ class Category extends SnipeModel
} }
switch ($this->category_type) { switch ($this->category_type) {
case 'asset': case 'asset':
return $this->assets->count(); return $this->assets->count();
case 'accessory': case 'accessory':
return $this->accessories->count(); return $this->accessories->count();
case 'component': case 'component':
return $this->components->count(); return $this->components->count();
case 'consumable': case 'consumable':
return $this->consumables->count(); return $this->consumables->count();
case 'license': case 'license':
return $this->licenses->count(); return $this->licenses->count();
default: default:
return 0; return 0;
} }
} }
@@ -191,7 +201,7 @@ class Category extends SnipeModel
* Establishes the category -> assets relationship * Establishes the category -> assets relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0] * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assets() public function assets()
@@ -208,8 +218,8 @@ class Category extends SnipeModel
* by their category. * by their category.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.0] * @since [v6.1.0]
* @see \App\Models\Asset::scopeAssetsForShow() * @see \App\Models\Asset::scopeAssetsForShow()
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function showableAssets() public function showableAssets()
@@ -221,7 +231,7 @@ class Category extends SnipeModel
* Establishes the category -> models relationship * Establishes the category -> models relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0] * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function models() public function models()
@@ -239,7 +249,7 @@ class Category extends SnipeModel
* checks for a settings level EULA * checks for a settings level EULA
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0] * @since [v2.0]
* @return string | null * @return string | null
*/ */
public function getEula() public function getEula()
@@ -266,7 +276,7 @@ class Category extends SnipeModel
* *
* This will also correctly parse a 1/0 if "true"/"false" is passed. * This will also correctly parse a 1/0 if "true"/"false" is passed.
* *
* @param $value * @param $value
* @return void * @return void
*/ */
public function setCheckinEmailAttribute($value) public function setCheckinEmailAttribute($value)
@@ -283,9 +293,9 @@ class Category extends SnipeModel
/** /**
* Query builder scope for whether or not the category requires acceptance * Query builder scope for whether or not the category requires acceptance
* *
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
public function scopeRequiresAcceptance($query) public function scopeRequiresAcceptance($query)

View File

@@ -15,6 +15,7 @@ class CheckoutAcceptance extends Model
protected $casts = [ protected $casts = [
'accepted_at' => 'datetime', 'accepted_at' => 'datetime',
'declined_at' => 'datetime', 'declined_at' => 'datetime',
'alert_on_response_id' => 'integer',
]; ];
/** /**
@@ -65,7 +66,7 @@ class CheckoutAcceptance extends Model
/** /**
* Was the checkoutable checked out to this user? * Was the checkoutable checked out to this user?
* *
* @param User $user * @param User $user
* @return bool * @return bool
*/ */
public function isCheckedOutTo(User $user) public function isCheckedOutTo(User $user)
@@ -78,7 +79,7 @@ class CheckoutAcceptance extends Model
* Do not add stuff here that doesn't have a corresponding column in the * Do not add stuff here that doesn't have a corresponding column in the
* checkout_acceptances table or you'll get an error. * checkout_acceptances table or you'll get an error.
* *
* @param string $signature_filename * @param string $signature_filename
*/ */
public function accept($signature_filename, $eula = null, $filename = null, $note = null) public function accept($signature_filename, $eula = null, $filename = null, $note = null)
{ {
@@ -98,7 +99,7 @@ class CheckoutAcceptance extends Model
/** /**
* Decline the checkout acceptance * Decline the checkout acceptance
* *
* @param string $signature_filename * @param string $signature_filename
*/ */
public function decline($signature_filename, $note = null) public function decline($signature_filename, $note = null)
{ {
@@ -115,8 +116,9 @@ class CheckoutAcceptance extends Model
/** /**
* Filter checkout acceptences by the user * Filter checkout acceptences by the user
*
* @param Illuminate\Database\Eloquent\Builder $query * @param Illuminate\Database\Eloquent\Builder $query
* @param User $user * @param User $user
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
public function scopeForUser(Builder $query, User $user) public function scopeForUser(Builder $query, User $user)
@@ -126,6 +128,7 @@ class CheckoutAcceptance extends Model
/** /**
* Filter to only get pending acceptances * Filter to only get pending acceptances
*
* @param Illuminate\Database\Eloquent\Builder $query * @param Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */

View File

@@ -13,11 +13,13 @@ use Illuminate\Support\Facades\Schema;
/** /**
* Model for Companies. * Model for Companies.
* *
* @version v1.8 * @version v1.8
*/ */
final class Company extends SnipeModel final class Company extends SnipeModel
{ {
use HasFactory; use HasFactory;
use CompanyableTrait;
protected $table = 'companies'; protected $table = 'companies';
@@ -26,19 +28,19 @@ final class Company extends SnipeModel
'name' => 'required|min:1|max:255|unique:companies,name', 'name' => 'required|min:1|max:255|unique:companies,name',
'fax' => 'min:7|max:35|nullable', 'fax' => 'min:7|max:35|nullable',
'phone' => 'min:7|max:35|nullable', 'phone' => 'min:7|max:35|nullable',
'email' => 'email|max:150|nullable', 'email' => 'email|max:150|nullable',
]; ];
protected $presenter = \App\Presenters\CompanyPresenter::class; protected $presenter = \App\Presenters\CompanyPresenter::class;
use Presentable; use Presentable;
/** /**
* Whether the model should inject it's identifier to the unique * Whether the model should inject it's identifier to the unique
* validation rules before attempting validation. If this property * validation rules before attempting validation. If this property
* is not set in the model it will default to true. * is not set in the model it will default to true.
* *
* @var bool * @var bool
*/ */
protected $injectUniqueIdentifier = true; protected $injectUniqueIdentifier = true;
use ValidatingTrait; use ValidatingTrait;
use Searchable; use Searchable;
@@ -100,7 +102,7 @@ final class Company extends SnipeModel
* account the full multiple company support setting * account the full multiple company support setting
* and if the current user is a super user. * and if the current user is a super user.
* *
* @param $unescaped_input * @param $unescaped_input
* @return int|mixed|string|null * @return int|mixed|string|null
*/ */
public static function getIdForCurrentUser($unescaped_input) public static function getIdForCurrentUser($unescaped_input)
@@ -127,7 +129,7 @@ final class Company extends SnipeModel
* Check to see if the current user should have access to the model. * Check to see if the current user should have access to the model.
* I hate this method and I think it should be refactored. * I hate this method and I think it should be refactored.
* *
* @param $companyable * @param $companyable
* @return bool|void * @return bool|void
*/ */
public static function isCurrentUserHasAccess($companyable) public static function isCurrentUserHasAccess($companyable)
@@ -146,10 +148,10 @@ final class Company extends SnipeModel
if (!is_string($companyable)) { if (!is_string($companyable)) {
$company_table = $companyable->getModel()->getTable(); $company_table = $companyable->getModel()->getTable();
try { try {
// This is primary for the gate:allows-check in location->isDeletable() // This is primarily for the gate:allows-check in location->isDeletable()
// Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled // Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled
// because this function is called by SnipePermissionsPolicy->before() // because this function is called by SnipePermissionsPolicy->before()
if (!$companyable instanceof Company && !Schema::hasColumn($company_table, 'company_id')) { if (!Schema::hasColumn($company_table, 'company_id')) {
return true; return true;
} }
@@ -160,12 +162,19 @@ final class Company extends SnipeModel
if (auth()->user()) { if (auth()->user()) {
Log::warning('Companyable is '.$companyable); // Log::warning('Companyable is '.$companyable);
$current_user_company_id = auth()->user()->company_id; $current_user_company_id = auth()->user()->company_id;
$companyable_company_id = $companyable->company_id; $companyable_company_id = $companyable->company_id;
return $current_user_company_id == null || $current_user_company_id == $companyable_company_id || auth()->user()->isSuperUser();
// Set this to check companyable on company
if ($companyable instanceof Company) {
$companyable_company_id = $companyable->id;
}
return ($current_user_company_id == null) || ($current_user_company_id == $companyable_company_id) || auth()->user()->isSuperUser();
} }
return false;
} }
public static function isCurrentUserAuthorized() public static function isCurrentUserAuthorized()
@@ -183,7 +192,7 @@ final class Company extends SnipeModel
* Checks if company can be deleted * Checks if company can be deleted
* *
* @author [Dan Meltzer] [<dmeltzer.devel@gmail.com>] * @author [Dan Meltzer] [<dmeltzer.devel@gmail.com>]
* @since [v5.0] * @since [v5.0]
* @return bool * @return bool
*/ */
public function isDeletable() public function isDeletable()
@@ -200,7 +209,7 @@ final class Company extends SnipeModel
} }
/** /**
* @param $unescaped_input * @param $unescaped_input
* @return int|mixed|string|null * @return int|mixed|string|null
*/ */
public static function getIdForUser($unescaped_input) public static function getIdForUser($unescaped_input)
@@ -256,14 +265,14 @@ final class Company extends SnipeModel
* @todo - refactor that trait to handle the user's model as well. * @todo - refactor that trait to handle the user's model as well.
* *
* @author [A. Gianotto] <snipe@snipe.net> * @author [A. Gianotto] <snipe@snipe.net>
* @param $query * @param $query
* @param $column * @param $column
* @param $table_name * @param $table_name
* @return mixed * @return mixed
*/ */
public static function scopeCompanyables($query, $column = 'company_id', $table_name = null) public static function scopeCompanyables($query, $column = 'company_id', $table_name = null)
{ {
// If not logged in and hitting this, assume we are on the command line and don't scope?' // If not logged in and hitting this, assume we are on the command line and don't scope?
if (! static::isFullMultipleCompanySupportEnabled() || (Auth::hasUser() && auth()->user()->isSuperUser()) || (! Auth::hasUser())) { if (! static::isFullMultipleCompanySupportEnabled() || (Auth::hasUser() && auth()->user()->isSuperUser()) || (! Auth::hasUser())) {
return $query; return $query;
} else { } else {
@@ -280,11 +289,16 @@ final class Company extends SnipeModel
private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null) private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null)
{ {
$company_id = null;
// Get the company ID of the logged-in user, or set it to null if there is no company associated with the user // Get the company ID of the logged-in user, or set it to null if there is no company associated with the user
if (Auth::hasUser()) { if (Auth::hasUser()) {
$company_id = auth()->user()->company_id; $company_id = auth()->user()->company_id;
} else { }
$company_id = null;
// If we are scoping the companies table itself, look for the company.id
if ($query->getModel()->getTable() == 'companies') {
return $query->where('companies.id', '=', $company_id);
} }
@@ -297,6 +311,8 @@ final class Company extends SnipeModel
return $query->where($table.$column, '=', $company_id); return $query->where($table.$column, '=', $company_id);
} }
} }
public function adminuser() public function adminuser()
@@ -311,8 +327,8 @@ final class Company extends SnipeModel
* This gets invoked by CompanyableChildScope, but I'm not sure what it does. * This gets invoked by CompanyableChildScope, but I'm not sure what it does.
* *
* @author [A. Gianotto] <snipe@snipe.net> * @author [A. Gianotto] <snipe@snipe.net>
* @param array $companyable_names * @param array $companyable_names
* @param $query * @param $query
* @return mixed * @return mixed
*/ */
public static function scopeCompanyableChildren(array $companyable_names, $query) public static function scopeCompanyableChildren(array $companyable_names, $query)
@@ -324,17 +340,18 @@ final class Company extends SnipeModel
return $query; return $query;
} else { } else {
$f = function ($q) { $f = function ($q) {
Log::debug('scopeCompanyablesDirectly firing ');
static::scopeCompanyablesDirectly($q); static::scopeCompanyablesDirectly($q);
}; };
$q = $query->where(function ($q) use ($companyable_names, $f) { $q = $query->where(
$q2 = $q->whereHas($companyable_names[0], $f); function ($q) use ($companyable_names, $f) {
$q2 = $q->whereHas($companyable_names[0], $f);
for ($i = 1; $i < count($companyable_names); $i++) { for ($i = 1; $i < count($companyable_names); $i++) {
$q2 = $q2->orWhereHas($companyable_names[$i], $f); $q2 = $q2->orWhereHas($companyable_names[$i], $f);
}
} }
}); );
return $q; return $q;
} }

View File

@@ -9,15 +9,15 @@ use Illuminate\Database\Eloquent\Scope;
/** /**
* Handle query scoping for full company support. * Handle query scoping for full company support.
* *
* @todo Move this to a more Laravel 5.2 esque way * @todo Move this to a more Laravel 5.2 esque way
* @version v1.0 * @version v1.0
*/ */
final class CompanyableChildScope implements Scope final class CompanyableChildScope implements Scope
{ {
/** /**
* Apply the scope to a given Eloquent query builder. * Apply the scope to a given Eloquent query builder.
* *
* @param \Illuminate\Database\Eloquent\Builder $builder * @param \Illuminate\Database\Eloquent\Builder $builder
* @return void * @return void
*/ */
public function apply(Builder $builder, Model $model) public function apply(Builder $builder, Model $model)
@@ -31,7 +31,7 @@ final class CompanyableChildScope implements Scope
* @todo IMPLEMENT * @todo IMPLEMENT
* Remove the scope from the given Eloquent query builder. * Remove the scope from the given Eloquent query builder.
* *
* @param \Illuminate\Database\Eloquent\Builder $builder * @param \Illuminate\Database\Eloquent\Builder $builder
* @return void * @return void
*/ */
public function remove(Builder $builder) public function remove(Builder $builder)

View File

@@ -9,15 +9,15 @@ use Illuminate\Database\Eloquent\Scope;
/** /**
* Handle query scoping for full company support. * Handle query scoping for full company support.
* *
* @todo Move this to a more Laravel 5.2 esque way * @todo Move this to a more Laravel 5.2 esque way
* @version v1.0 * @version v1.0
*/ */
final class CompanyableScope implements Scope final class CompanyableScope implements Scope
{ {
/** /**
* Apply the scope to a given Eloquent query builder. * Apply the scope to a given Eloquent query builder.
* *
* @param \Illuminate\Database\Eloquent\Builder $builder * @param \Illuminate\Database\Eloquent\Builder $builder
* @return void * @return void
*/ */
public function apply(Builder $builder, Model $model) public function apply(Builder $builder, Model $model)
@@ -29,7 +29,7 @@ final class CompanyableScope implements Scope
* @todo IMPLEMENT * @todo IMPLEMENT
* Remove the scope from the given Eloquent query builder. * Remove the scope from the given Eloquent query builder.
* *
* @param \Illuminate\Database\Eloquent\Builder $builder * @param \Illuminate\Database\Eloquent\Builder $builder
* @return void * @return void
*/ */
public function remove(Builder $builder) public function remove(Builder $builder)

View File

@@ -8,7 +8,7 @@ trait CompanyableTrait
* This trait is used to scope models to the current company. To use this scope on companyable models, * This trait is used to scope models to the current company. To use this scope on companyable models,
* we use the "use Companyable;" statement at the top of the mode. * we use the "use Companyable;" statement at the top of the mode.
* *
* @see \App\Models\Company\Company::scopeCompanyables() * @see \App\Models\Company\Company::scopeCompanyables()
* @return void * @return void
*/ */
public static function bootCompanyableTrait() public static function bootCompanyableTrait()

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable; use App\Models\Traits\Searchable;
use App\Presenters\Presentable; use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -12,7 +13,7 @@ use Watson\Validating\ValidatingTrait;
/** /**
* Model for Components. * Model for Components.
* *
* @version v1.0 * @version v1.0
*/ */
class Component extends SnipeModel class Component extends SnipeModel
{ {
@@ -20,6 +21,7 @@ class Component extends SnipeModel
protected $presenter = \App\Presenters\ComponentPresenter::class; protected $presenter = \App\Presenters\ComponentPresenter::class;
use CompanyableTrait; use CompanyableTrait;
use HasUploads;
use Loggable, Presentable; use Loggable, Presentable;
use SoftDeletes; use SoftDeletes;
protected $casts = [ protected $casts = [
@@ -113,28 +115,13 @@ class Component extends SnipeModel
&& ($this->deleted_at == ''); && ($this->deleted_at == '');
} }
/**
* Establishes the components -> action logs -> uploads relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.1.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
{
return $this->hasMany(\App\Models\Actionlog::class, 'item_id')
->where('item_type', '=', self::class)
->where('action_type', '=', 'uploaded')
->whereNotNull('filename')
->orderBy('created_at', 'desc');
}
/** /**
* Establishes the component -> location relationship * Establishes the component -> location relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function location() public function location()
@@ -146,7 +133,7 @@ class Component extends SnipeModel
* Establishes the component -> assets relationship * Establishes the component -> assets relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assets() public function assets()
@@ -160,7 +147,7 @@ class Component extends SnipeModel
* @todo this is probably not needed - refactor * @todo this is probably not needed - refactor
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function adminuser() public function adminuser()
@@ -172,7 +159,7 @@ class Component extends SnipeModel
* Establishes the component -> company relationship * Establishes the component -> company relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function company() public function company()
@@ -184,7 +171,7 @@ class Component extends SnipeModel
* Establishes the component -> category relationship * Establishes the component -> category relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function category() public function category()
@@ -196,7 +183,7 @@ class Component extends SnipeModel
* Establishes the item -> supplier relationship * Establishes the item -> supplier relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.1] * @since [v6.1.1]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function supplier() public function supplier()
@@ -209,7 +196,7 @@ class Component extends SnipeModel
* Establishes the item -> manufacturer relationship * Establishes the item -> manufacturer relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function manufacturer() public function manufacturer()
@@ -221,7 +208,7 @@ class Component extends SnipeModel
* Establishes the component -> action logs relationship * Establishes the component -> action logs relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assetlog() public function assetlog()
@@ -233,7 +220,7 @@ class Component extends SnipeModel
* Check how many items within a component are checked out * Check how many items within a component are checked out
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0] * @since [v5.0]
* @return int * @return int
*/ */
public function numCheckedOut() public function numCheckedOut()
@@ -252,11 +239,12 @@ class Component extends SnipeModel
* *
* This allows us to get the assets with assigned components without the company restriction * This allows us to get the assets with assigned components without the company restriction
*/ */
public function uncontrainedAssets() { public function uncontrainedAssets()
{
return $this->belongsToMany(\App\Models\Asset::class, 'components_assets') return $this->belongsToMany(\App\Models\Asset::class, 'components_assets')
->withPivot('id', 'assigned_qty', 'created_at', 'created_by', 'note') ->withPivot('id', 'assigned_qty', 'created_at', 'created_by', 'note')
->withoutGlobalScope(new CompanyableScope); ->withoutGlobalScope(new CompanyableScope);
} }
@@ -265,7 +253,7 @@ class Component extends SnipeModel
* Check how many items within a component are remaining * Check how many items within a component are remaining
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return int * @return int
*/ */
public function numRemaining() public function numRemaining()
@@ -288,8 +276,8 @@ class Component extends SnipeModel
* This simply checks that there is a value for quantity, and if there isn't, set it to 0. * This simply checks that there is a value for quantity, and if there isn't, set it to 0.
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4 * @since v6.3.4
* @param $value * @param $value
* @return void * @return void
*/ */
public function setQtyAttribute($value) public function setQtyAttribute($value)
@@ -307,8 +295,8 @@ class Component extends SnipeModel
/** /**
* Query builder scope to order on company * Query builder scope to order on company
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -320,8 +308,8 @@ class Component extends SnipeModel
/** /**
* Query builder scope to order on company * Query builder scope to order on company
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -333,8 +321,8 @@ class Component extends SnipeModel
/** /**
* Query builder scope to order on company * Query builder scope to order on company
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -346,8 +334,8 @@ class Component extends SnipeModel
/** /**
* Query builder scope to order on supplier * Query builder scope to order on supplier
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -359,8 +347,8 @@ class Component extends SnipeModel
/** /**
* Query builder scope to order on manufacturer * Query builder scope to order on manufacturer
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Models\Traits\Acceptable; use App\Models\Traits\Acceptable;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable; use App\Models\Traits\Searchable;
use App\Presenters\Presentable; use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -29,6 +30,7 @@ class Consumable extends SnipeModel
use Loggable, Presentable; use Loggable, Presentable;
use SoftDeletes; use SoftDeletes;
use Acceptable; use Acceptable;
use HasUploads;
protected $table = 'consumables'; protected $table = 'consumables';
protected $casts = [ protected $casts = [
@@ -111,21 +113,6 @@ class Consumable extends SnipeModel
]; ];
/**
* Establishes the components -> action logs -> uploads relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.1.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
{
return $this->hasMany(Actionlog::class, 'item_id')
->where('item_type', '=', self::class)
->where('action_type', '=', 'uploaded')
->whereNotNull('filename')
->orderBy('created_at', 'desc');
}
/** /**
@@ -138,7 +125,7 @@ class Consumable extends SnipeModel
* @todo Update this comment once it's been implemented * @todo Update this comment once it's been implemented
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function setRequestableAttribute($value) public function setRequestableAttribute($value)
@@ -153,7 +140,7 @@ class Consumable extends SnipeModel
* Establishes the consumable -> admin user relationship * Establishes the consumable -> admin user relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function adminuser() public function adminuser()
@@ -165,7 +152,7 @@ class Consumable extends SnipeModel
* Establishes the component -> assignments relationship * Establishes the component -> assignments relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function consumableAssignments() public function consumableAssignments()
@@ -177,7 +164,7 @@ class Consumable extends SnipeModel
* Establishes the component -> company relationship * Establishes the component -> company relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function company() public function company()
@@ -189,7 +176,7 @@ class Consumable extends SnipeModel
* Establishes the component -> manufacturer relationship * Establishes the component -> manufacturer relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function manufacturer() public function manufacturer()
@@ -201,7 +188,7 @@ class Consumable extends SnipeModel
* Establishes the component -> location relationship * Establishes the component -> location relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function location() public function location()
@@ -213,7 +200,7 @@ class Consumable extends SnipeModel
* Establishes the component -> category relationship * Establishes the component -> category relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function category() public function category()
@@ -226,7 +213,7 @@ class Consumable extends SnipeModel
* Establishes the component -> action logs relationship * Establishes the component -> action logs relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assetlog() public function assetlog()
@@ -238,7 +225,7 @@ class Consumable extends SnipeModel
* Gets the full image url for the consumable * Gets the full image url for the consumable
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return string | false * @return string | false
*/ */
public function getImageUrl() public function getImageUrl()
@@ -254,7 +241,7 @@ class Consumable extends SnipeModel
* Establishes the component -> users relationship * Establishes the component -> users relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
*/ */
public function users() : Relation public function users() : Relation
{ {
@@ -265,7 +252,7 @@ class Consumable extends SnipeModel
* Establishes the item -> supplier relationship * Establishes the item -> supplier relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.1.1] * @since [v6.1.1]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function supplier() public function supplier()
@@ -279,19 +266,19 @@ class Consumable extends SnipeModel
* asset model category * asset model category
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
* @return bool * @return bool
*/ */
public function checkin_email() public function checkin_email()
{ {
return $this->category->checkin_email; return $this->category?->checkin_email;
} }
/** /**
* Determine whether this asset requires acceptance by the assigned user * Determine whether this asset requires acceptance by the assigned user
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
* @return bool * @return bool
*/ */
public function requireAcceptance() public function requireAcceptance()
@@ -304,7 +291,7 @@ class Consumable extends SnipeModel
* checks for a settings level EULA * checks for a settings level EULA
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
* @return string | false * @return string | false
*/ */
public function getEula() public function getEula()
@@ -322,7 +309,7 @@ class Consumable extends SnipeModel
* Check how many items within a consumable are checked out * Check how many items within a consumable are checked out
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v5.0] * @since [v5.0]
* @return int * @return int
*/ */
public function numCheckedOut() public function numCheckedOut()
@@ -334,7 +321,7 @@ class Consumable extends SnipeModel
* Checks the number of available consumables * Checks the number of available consumables
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0] * @since [v4.0]
* @return int * @return int
*/ */
public function numRemaining() public function numRemaining()
@@ -360,8 +347,8 @@ class Consumable extends SnipeModel
* This simply checks that there is a value for quantity, and if there isn't, set it to 0. * This simply checks that there is a value for quantity, and if there isn't, set it to 0.
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since v6.3.4 * @since v6.3.4
* @param $value * @param $value
* @return void * @return void
*/ */
public function setQtyAttribute($value) public function setQtyAttribute($value)
@@ -378,8 +365,8 @@ class Consumable extends SnipeModel
/** /**
* Query builder scope to order on company * Query builder scope to order on company
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -391,8 +378,8 @@ class Consumable extends SnipeModel
/** /**
* Query builder scope to order on location * Query builder scope to order on location
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -404,8 +391,8 @@ class Consumable extends SnipeModel
/** /**
* Query builder scope to order on manufacturer * Query builder scope to order on manufacturer
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -417,8 +404,8 @@ class Consumable extends SnipeModel
/** /**
* Query builder scope to order on company * Query builder scope to order on company
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -430,8 +417,8 @@ class Consumable extends SnipeModel
/** /**
* Query builder scope to order on remaining * Query builder scope to order on remaining
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order * @param string $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -444,8 +431,8 @@ class Consumable extends SnipeModel
/** /**
* Query builder scope to order on supplier * Query builder scope to order on supplier
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */

View File

@@ -92,7 +92,7 @@ class CustomField extends Model
* table instead of the assets table. * table instead of the assets table.
* *
* @author [Brady Wetherington] [<uberbrady@gmail.com>] * @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0] * @since [v3.0]
*/ */
public static $table_name = 'assets'; public static $table_name = 'assets';
@@ -103,7 +103,7 @@ class CustomField extends Model
* do with previously existing values. - @snipe * do with previously existing values. - @snipe
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4] * @since [v3.4]
* @return string * @return string
*/ */
public static function name_to_db_name($name) public static function name_to_db_name($name)
@@ -120,66 +120,78 @@ class CustomField extends Model
* to do it in the controllers. * to do it in the controllers.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4] * @since [v3.4]
* @return bool * @return bool
*/ */
public static function boot() public static function boot()
{ {
parent::boot(); parent::boot();
self::created(function ($custom_field) { self::created(
function ($custom_field) {
// Column already exists on the assets table - nothing to do here. // Column already exists on the assets table - nothing to do here.
// This *shouldn't* happen in the wild. // This *shouldn't* happen in the wild.
if (Schema::hasColumn(self::$table_name, $custom_field->db_column)) { if (Schema::hasColumn(self::$table_name, $custom_field->db_column)) {
return false; return false;
}
// Update the column name in the assets table
Schema::table(
self::$table_name, function ($table) use ($custom_field) {
$table->text($custom_field->convertUnicodeDbSlug())->nullable();
}
);
// Update the db_column property in the custom fields table
$custom_field->db_column = $custom_field->convertUnicodeDbSlug();
$custom_field->save();
} }
);
// Update the column name in the assets table self::updating(
Schema::table(self::$table_name, function ($table) use ($custom_field) { function ($custom_field) {
$table->text($custom_field->convertUnicodeDbSlug())->nullable();
});
// Update the db_column property in the custom fields table // Column already exists on the assets table - nothing to do here.
$custom_field->db_column = $custom_field->convertUnicodeDbSlug(); if ($custom_field->isDirty('name')) {
$custom_field->save(); if (Schema::hasColumn(self::$table_name, $custom_field->convertUnicodeDbSlug())) {
}); return true;
}
self::updating(function ($custom_field) { // Rename the field if the name has changed
Schema::table(
self::$table_name, function ($table) use ($custom_field) {
$table->renameColumn($custom_field->convertUnicodeDbSlug($custom_field->getOriginal('name')), $custom_field->convertUnicodeDbSlug());
}
);
// Save the updated column name to the custom fields table
$custom_field->db_column = $custom_field->convertUnicodeDbSlug();
$custom_field->save();
// Column already exists on the assets table - nothing to do here.
if ($custom_field->isDirty('name')) {
if (Schema::hasColumn(self::$table_name, $custom_field->convertUnicodeDbSlug())) {
return true; return true;
} }
// Rename the field if the name has changed
Schema::table(self::$table_name, function ($table) use ($custom_field) {
$table->renameColumn($custom_field->convertUnicodeDbSlug($custom_field->getOriginal('name')), $custom_field->convertUnicodeDbSlug());
});
// Save the updated column name to the custom fields table
$custom_field->db_column = $custom_field->convertUnicodeDbSlug();
$custom_field->save();
return true; return true;
} }
);
return true;
});
// Drop the assets column if we've deleted it from custom fields // Drop the assets column if we've deleted it from custom fields
self::deleting(function ($custom_field) { self::deleting(
return Schema::table(self::$table_name, function ($table) use ($custom_field) { function ($custom_field) {
$table->dropColumn($custom_field->db_column); return Schema::table(
}); self::$table_name, function ($table) use ($custom_field) {
}); $table->dropColumn($custom_field->db_column);
}
);
}
);
} }
/** /**
* Establishes the customfield -> fieldset relationship * Establishes the customfield -> fieldset relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function fieldset() public function fieldset()
@@ -214,26 +226,26 @@ class CustomField extends Model
public function displayFieldInCurrentForm($form_type = null) public function displayFieldInCurrentForm($form_type = null)
{ {
switch ($form_type) { switch ($form_type) {
case 'audit': case 'audit':
return $this->displayFieldInAuditForm(); return $this->displayFieldInAuditForm();
case 'checkin': case 'checkin':
return $this->displayFieldInCheckinForm(); return $this->displayFieldInCheckinForm();
case 'checkout': case 'checkout':
return $this->displayFieldInCheckoutForm(); return $this->displayFieldInCheckoutForm();
} }
} }
public function assetModels() public function assetModels()
{ {
return $this->fieldset()->with('models')->get()->pluck('models')->flatten()->unique('id'); return $this->fieldset()->with('models')->get()->pluck('models')->flatten()->unique('id');
} }
/** /**
* Establishes the customfield -> admin user relationship * Establishes the customfield -> admin user relationship
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function user() public function user()
@@ -245,7 +257,7 @@ class CustomField extends Model
* Establishes the customfield -> default values relationship * Establishes the customfield -> default values relationship
* *
* @author Hannah Tinkler * @author Hannah Tinkler
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function defaultValues() public function defaultValues()
@@ -262,19 +274,23 @@ class CustomField extends Model
*/ */
public function defaultValue($modelId) public function defaultValue($modelId)
{ {
return $this->defaultValues->filter(function ($item) use ($modelId) { return $this->defaultValues->filter(
return $item->pivot->asset_model_id == $modelId; function ($item) use ($modelId) {
})->map(function ($item) { return $item->pivot->asset_model_id == $modelId;
return $item->pivot->default_value; }
})->first(); )->map(
function ($item) {
return $item->pivot->default_value;
}
)->first();
} }
/** /**
* Checks the format of the attribute * Checks the format of the attribute
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @param $value string * @param $value string
* @since [v3.0] * @since [v3.0]
* @return bool * @return bool
*/ */
public function check_format($value) public function check_format($value)
@@ -286,7 +302,7 @@ class CustomField extends Model
* Gets the DB column name. * Gets the DB column name.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return string * @return string
*/ */
public function db_column_name() public function db_column_name()
@@ -302,7 +318,7 @@ class CustomField extends Model
* user-friendly text in the dropdowns, and in the custom fields display. * user-friendly text in the dropdowns, and in the custom fields display.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4] * @since [v3.4]
* @return string * @return string
*/ */
public function getFormatAttribute($value) public function getFormatAttribute($value)
@@ -320,7 +336,7 @@ class CustomField extends Model
* Format a value string as an array for select boxes and checkboxes. * Format a value string as an array for select boxes and checkboxes.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4] * @since [v3.4]
* @return array * @return array
*/ */
public function setFormatAttribute($value) public function setFormatAttribute($value)
@@ -336,7 +352,7 @@ class CustomField extends Model
* Format a value string as an array for select boxes and checkboxes. * Format a value string as an array for select boxes and checkboxes.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4] * @since [v3.4]
* @return array * @return array
*/ */
public function formatFieldValuesAsArray() public function formatFieldValuesAsArray()
@@ -366,7 +382,7 @@ class CustomField extends Model
* Check whether the field is encrypted * Check whether the field is encrypted
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4] * @since [v3.4]
* @return bool * @return bool
*/ */
public function isFieldDecryptable($string) public function isFieldDecryptable($string)
@@ -383,7 +399,7 @@ class CustomField extends Model
* won't break the database. * won't break the database.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4] * @since [v3.4]
* @return string * @return string
*/ */
public function convertUnicodeDbSlug($original = null) public function convertUnicodeDbSlug($original = null)
@@ -392,7 +408,7 @@ class CustomField extends Model
$id = $this->id ? $this->id : 'xx'; $id = $this->id ? $this->id : 'xx';
if (! function_exists('transliterator_transliterate')) { if (! function_exists('transliterator_transliterate')) {
$long_slug = '_snipeit_'.str_slug(mb_convert_encoding(trim($name),"UTF-8"), '_'); $long_slug = '_snipeit_'.str_slug(mb_convert_encoding(trim($name), "UTF-8"), '_');
} else { } else {
$long_slug = '_snipeit_'.Utf8Slugger::slugify($name, '_'); $long_slug = '_snipeit_'.Utf8Slugger::slugify($name, '_');
} }
@@ -402,9 +418,10 @@ class CustomField extends Model
/** /**
* Get validation rules for custom fields to use with Validator * Get validation rules for custom fields to use with Validator
*
* @author [V. Cordes] [<volker@fdatek.de>] * @author [V. Cordes] [<volker@fdatek.de>]
* @param int $id * @param int $id
* @since [v4.1.10] * @since [v4.1.10]
* @return array * @return array
*/ */
public function validationRules($regex_format = null) public function validationRules($regex_format = null)
@@ -418,6 +435,7 @@ class CustomField extends Model
/** /**
* Check to see if there is a custom regex format type * Check to see if there is a custom regex format type
*
* @see https://github.com/grokability/snipe-it/issues/5896 * @see https://github.com/grokability/snipe-it/issues/5896
* *
* @author Wes Hulette <jwhulette@gmail.com> * @author Wes Hulette <jwhulette@gmail.com>

View File

@@ -20,6 +20,7 @@ class CustomFieldset extends Model
/** /**
* Validation rules * Validation rules
*
* @var array * @var array
*/ */
public $rules = [ public $rules = [
@@ -39,7 +40,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> field relationship * Establishes the fieldset -> field relationship
* *
* @author [Brady Wetherington] [<uberbrady@gmail.com>] * @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function fields() public function fields()
@@ -51,7 +52,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> models relationship * Establishes the fieldset -> models relationship
* *
* @author [Brady Wetherington] [<uberbrady@gmail.com>] * @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function models() public function models()
@@ -63,7 +64,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> admin user relationship * Establishes the fieldset -> admin user relationship
* *
* @author [Brady Wetherington] [<uberbrady@gmail.com>] * @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0] * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function user() public function user()
@@ -76,14 +77,14 @@ class CustomFieldset extends Model
if ($this->fields) { if ($this->fields) {
switch ($form_type) { switch ($form_type) {
case 'audit': case 'audit':
return $this->fields->where('display_audit', '1')->count() > 0; return $this->fields->where('display_audit', '1')->count() > 0;
case 'checkin': case 'checkin':
return $this->fields->where('display_checkin', '1')->count() > 0; return $this->fields->where('display_checkin', '1')->count() > 0;
case 'checkout': case 'checkout':
return $this->fields->where('display_checkout', '1')->count() > 0; return $this->fields->where('display_checkout', '1')->count() > 0;
default: default:
return true; return true;
} }
} }
@@ -95,7 +96,7 @@ class CustomFieldset extends Model
* custom field format * custom field format
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0] * @since [v3.0]
* @return array * @return array
*/ */
public function validation_rules() public function validation_rules()
@@ -104,8 +105,9 @@ class CustomFieldset extends Model
foreach ($this->fields as $field) { foreach ($this->fields as $field) {
$rule = []; $rule = [];
if (($field->field_encrypted != '1') || if (($field->field_encrypted != '1')
(($field->field_encrypted == '1') && (Gate::allows('admin')))) { || (($field->field_encrypted == '1') && (Gate::allows('admin')))
) {
$rule[] = ($field->pivot->required == '1') ? 'required' : 'nullable'; $rule[] = ($field->pivot->required == '1') ? 'required' : 'nullable';
} }

View File

@@ -72,7 +72,7 @@ class Department extends SnipeModel
* Establishes the department -> company relationship * Establishes the department -> company relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v4.0] * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function company() public function company()
@@ -84,7 +84,7 @@ class Department extends SnipeModel
* Establishes the department -> users relationship * Establishes the department -> users relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v4.0] * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function users() public function users()
@@ -96,7 +96,7 @@ class Department extends SnipeModel
* Establishes the department -> manager relationship * Establishes the department -> manager relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v4.0] * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function manager() public function manager()
@@ -108,7 +108,7 @@ class Department extends SnipeModel
* Establishes the department -> location relationship * Establishes the department -> location relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v4.0] * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function location() public function location()
@@ -119,8 +119,8 @@ class Department extends SnipeModel
/** /**
* Query builder scope to order on location name * Query builder scope to order on location name
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -132,8 +132,8 @@ class Department extends SnipeModel
/** /**
* Query builder scope to order on manager name * Query builder scope to order on manager name
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */
@@ -145,8 +145,8 @@ class Department extends SnipeModel
/** /**
* Query builder scope to order on company * Query builder scope to order on company
* *
* @param \Illuminate\Database\Query\Builder $query Query builder instance * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $order Order * @param text $order Order
* *
* @return \Illuminate\Database\Query\Builder Modified query builder * @return \Illuminate\Database\Query\Builder Modified query builder
*/ */

View File

@@ -48,15 +48,15 @@ class Depreciable extends SnipeModel
$depreciation = 0; $depreciation = 0;
$setting = Setting::getSettings(); $setting = Setting::getSettings();
switch ($setting->depreciation_method) { switch ($setting->depreciation_method) {
case 'half_1': case 'half_1':
$depreciation = $this->getHalfYearDepreciatedValue(true); $depreciation = $this->getHalfYearDepreciatedValue(true);
break; break;
case 'half_2': case 'half_2':
$depreciation = $this->getHalfYearDepreciatedValue(false); $depreciation = $this->getHalfYearDepreciatedValue(false);
break; break;
default: default:
$depreciation = $this->getLinearDepreciatedValue(); $depreciation = $this->getLinearDepreciatedValue();
} }
@@ -74,7 +74,7 @@ class Depreciable extends SnipeModel
return null; return null;
} }
if ($months_passed >= $this->get_depreciation()->months){ if ($months_passed >= $this->get_depreciation()->months) {
//if there is a floor use it //if there is a floor use it
if($this->get_depreciation()->depreciation_min) { if($this->get_depreciation()->depreciation_min) {
@@ -93,14 +93,15 @@ class Depreciable extends SnipeModel
return $current_value; return $current_value;
} }
public function getMonthlyDepreciation(){ public function getMonthlyDepreciation()
{
return ($this->purchase_cost-$this->calculateDepreciation())/$this->get_depreciation()->months; return ($this->purchase_cost-$this->calculateDepreciation())/$this->get_depreciation()->months;
} }
/** /**
* @param onlyHalfFirstYear Boolean always applied only second half of the first year * @param onlyHalfFirstYear Boolean always applied only second half of the first year
* @return float|int * @return float|int
*/ */
public function getHalfYearDepreciatedValue($onlyHalfFirstYear = false) public function getHalfYearDepreciatedValue($onlyHalfFirstYear = false)
@@ -131,7 +132,7 @@ class Depreciable extends SnipeModel
} }
/** /**
* @param \DateTime $date * @param \DateTime $date
* @return int * @return int
*/ */
protected function get_fiscal_year($date) protected function get_fiscal_year($date)
@@ -146,7 +147,7 @@ class Depreciable extends SnipeModel
} }
/** /**
* @param \DateTime $date * @param \DateTime $date
* @return bool * @return bool
*/ */
protected function is_first_half_of_year($date) protected function is_first_half_of_year($date)

View File

@@ -16,7 +16,7 @@ class Depreciation extends SnipeModel
// Declare the rules for the form validation // Declare the rules for the form validation
protected $rules = [ protected $rules = [
'name' => 'required|min:3|max:255|unique:depreciations,name', 'name' => 'required|min:3|max:255|unique:depreciations,name',
'months' => 'required|max:3600|integer|gt:0', 'months' => 'required|max:3600|integer',
]; ];
/** /**
@@ -56,7 +56,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> models relationship * Establishes the depreciation -> models relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v5.0] * @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function models() public function models()
@@ -68,7 +68,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> licenses relationship * Establishes the depreciation -> licenses relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v5.0] * @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function licenses() public function licenses()
@@ -80,7 +80,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> assets relationship * Establishes the depreciation -> assets relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v5.0] * @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assets() public function assets()
@@ -92,7 +92,7 @@ class Depreciation extends SnipeModel
* Get the user that created the depreciation * Get the user that created the depreciation
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v7.0.13] * @since [v7.0.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function adminuser() public function adminuser()

View File

@@ -51,7 +51,7 @@ class Group extends SnipeModel
* Establishes the groups -> users relationship * Establishes the groups -> users relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v1.0] * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function users() public function users()
@@ -63,7 +63,7 @@ class Group extends SnipeModel
* Get the user that created the group * Get the user that created the group
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v6.3.0] * @since [v6.3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function adminuser() public function adminuser()
@@ -75,7 +75,7 @@ class Group extends SnipeModel
* Decode JSON permissions into array * Decode JSON permissions into array
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @since [v1.0] * @since [v1.0]
* @return array | \stdClass * @return array | \stdClass
*/ */
public function decodePermissions() public function decodePermissions()

Some files were not shown because too many files have changed in this diff Show More