Compare commits

...

469 Commits

Author SHA1 Message Date
snipe
750c376725 Set item to null so it doesn’t get merged automatically 2025-09-29 13:29:00 +01:00
snipe
08630d948f Remove radio buttons - they don’t work correctly yet 2025-09-29 12:43:19 +01:00
snipe
dc6ee342c5 Revert custom field name text box for now 2025-09-29 10:42:40 +01:00
snipe
eea1922841 Added checkbox and radios 2025-09-25 13:22:52 +01:00
snipe
57824848e9 Update resources/views/blade/form-row.blade.php
Co-authored-by: Marcus Moore <mmoore@grokability.com>
2025-09-24 17:08:00 +01:00
snipe
235dcbc7d9 Updated more views 2025-09-24 16:58:35 +01:00
snipe
f0561475cc Update resources/views/blade/input/text.blade.php
Co-authored-by: Marcus Moore <mmoore@grokability.com>
2025-09-24 16:41:56 +01:00
snipe
3b4c51bab6 Use slot instead of label 2025-09-24 14:52:29 +01:00
snipe
150c205615 Added checkboxes to status labels page 2025-09-23 21:37:17 +01:00
snipe
cde22977b0 More checkbox changes 2025-09-23 21:23:03 +01:00
snipe
1c09657631 Handle checkboxes 2025-09-23 20:57:33 +01:00
snipe
02a4268180 Switched more fields to blade compaonents 2025-09-23 19:52:58 +01:00
snipe
f8362f4a45 handled text fields in status labels 2025-09-23 18:44:06 +01:00
snipe
682c1a8fa7 Make required false 2025-09-23 18:43:50 +01:00
snipe
3863e82dcc Handle text and date fields on license edit 2025-09-23 18:43:39 +01:00
snipe
b766f6e2b5 Made edit screen narrower on wide screens 2025-09-23 18:43:14 +01:00
snipe
a3bad98096 Added date and tooltip components 2025-09-23 18:43:02 +01:00
snipe
33e7425dee Account for tooltips 2025-09-23 18:42:46 +01:00
snipe
2554b50b38 Updated suppliers 2025-09-23 17:04:49 +01:00
snipe
433a3e11fd Fixed field type 2025-09-23 17:00:03 +01:00
snipe
c3efdd0c8d Updated company 2025-09-23 16:59:09 +01:00
snipe
d2e3a13043 Added ability to mark fields as disabled 2025-09-23 16:58:53 +01:00
snipe
c4923fa971 Updated address 2025-09-23 16:53:52 +01:00
snipe
253026de5d Updated locations edit 2025-09-23 16:51:55 +01:00
snipe
a59914e9f9 Added placeholder back in 2025-09-23 16:38:12 +01:00
snipe
671e79f01b Sorry, this shouldn’t have been in there :( 2025-09-23 16:35:04 +01:00
snipe
5c716c3f24 Updated notes maxlength 2025-09-23 16:31:20 +01:00
snipe
e138c9307e Added maxlength default for text 2025-09-23 16:30:18 +01:00
snipe
7c0c3b2bb8 Use shorthand for errors 2025-09-23 16:26:28 +01:00
snipe
514711ddbb Label blade component 2025-09-23 16:18:00 +01:00
snipe
9023eda66f Changed class 2025-09-23 14:25:52 +01:00
snipe
8ce3001ef9 First stab 2025-09-23 14:25:40 +01:00
snipe
25ce63f00b Merge pull request #17904 from grokability/#17804-searchable-columns
Fixed #17804 - make columns searchable in column picker
2025-09-19 12:43:46 +01:00
snipe
2462bc05b3 Added column search to additional views 2025-09-19 12:41:30 +01:00
snipe
c3748da0b1 Fixed #17804 - make columns searchable in column picker 2025-09-19 12:16:56 +01:00
snipe
90c242a441 Merge pull request #17897 from marcusmoore/fixes/17896-prevent-bulk-checkout-of-checked-out-assets
Fixed #17896 - Prevent assigned assets from being bulk checked out
2025-09-19 07:03:43 +01:00
Marcus Moore
52239a88b5 Improve test name 2025-09-18 17:27:17 -07:00
Marcus Moore
7a3596c86d Test against other types 2025-09-18 17:21:27 -07:00
Marcus Moore
ac8a9e38f0 Implement fix 2025-09-18 17:18:27 -07:00
Marcus Moore
5c08f3a27e Add failing test 2025-09-18 17:14:33 -07:00
snipe
5216dd75bf Bumped version 2025-09-18 14:49:15 +01:00
snipe
b8b45d2d81 Merge pull request #17892 from grokability/#17891-fixes-maintenance-file-route
Fixed #17891 - missing maintenance file deletion route
2025-09-18 13:59:10 +01:00
snipe
4b2b2cb68e Fixed #17891 - missing maintenance file deletion route 2025-09-18 13:58:30 +01:00
snipe
be4ace293e Use trans_choice for user acceptance 2025-09-18 13:51:57 +01:00
snipe
764b363bbc A few small tweaks to acceptance screen design 2025-09-18 13:38:37 +01:00
snipe
9da9166442 Merge pull request #17886 from grokability/small-tweaks-to-acceptance-pdf
Small adjustments for acceptance PDF layout
2025-09-17 22:01:41 +01:00
snipe
8ea339f0ef More small tweaks 2025-09-17 22:00:49 +01:00
snipe
89b36ba63f Derp. Uncomment the acceptance. 2025-09-17 21:43:40 +01:00
snipe
1d3dfa1fa4 Pull the acceptance stuff into the model 2025-09-17 21:43:17 +01:00
snipe
ca567eec8a Small adjustments for layout 2025-09-17 21:08:13 +01:00
snipe
41da31c379 Merge pull request #17885 from grokability/#8859-show-cost-footer-on-models
Fixed #8859 - adds purchase sums on model view
2025-09-17 14:04:38 +01:00
snipe
e81f63f46b Fixed #8859 - adds purchase sums on model view 2025-09-17 14:03:48 +01:00
snipe
ade03e4827 Merge pull request #17882 from Godmartinz/add-total-cost-columns
Adds total cost to Accessories, Consumables, Components
2025-09-17 13:56:08 +01:00
snipe
33a4c88c3a Added table to deleted_at clauses to resolve ambiguity 2025-09-17 11:44:28 +01:00
snipe
1f79776b8f Pull HTML tags out before converting markdown 2025-09-16 19:21:39 +01:00
Godfrey M
11e5f851f0 typo 2025-09-16 10:49:33 -07:00
Godfrey M
4ca1db8a1b remove footer formatter from consumable purchase cost 2025-09-16 10:43:02 -07:00
Godfrey M
14b829aa30 add total cost to components and consumables 2025-09-16 10:37:32 -07:00
Godfrey M
384652b3df add total cost to accessories 2025-09-16 10:10:49 -07:00
snipe
9db65c6ae9 Merge pull request #17881 from grokability/#17873-eula-tab-on-users
Fixed #17873 - Added EULA tab to user view
2025-09-16 14:28:50 +01:00
snipe
1346e33e99 Check that the person trying to download can see both the user and the target 2025-09-16 14:21:03 +01:00
snipe
ab9cc447aa Use more specific filename 2025-09-16 14:20:20 +01:00
snipe
fe9e0444b4 Added EULA tab to user view 2025-09-16 13:20:50 +01:00
snipe
a18957dbe9 Include output even if there is nothing to send 2025-09-16 12:33:06 +01:00
snipe
13d5b724ee Fixed tests 2025-09-16 12:16:18 +01:00
snipe
c7d8203da9 Merge pull request #17866 from grokability/_reworked_tcpdf
Fixed #14744 and #17808 - Added CJK and Arabic font support for asset acceptance
2025-09-16 12:04:15 +01:00
snipe
96b5c1d8e1 Merge pull request #17876 from uberbrady/add_users_location_index
Add new index to users over deleted_at and location_id
2025-09-16 12:03:45 +01:00
Brady Wetherington
882ee80424 Add new index to users over deleted_at and location_id 2025-09-16 11:54:24 +01:00
snipe
e977771fe4 One more query tweak 2025-09-16 11:50:50 +01:00
snipe
4339e4552e Fixed nesting in orWhere 2025-09-16 11:50:50 +01:00
snipe
b54d222943 One more query tweak 2025-09-16 11:39:02 +01:00
snipe
e4e613550a Merge pull request #17875 from grokability/fixed-eol-query
Fixed nesting in orWhere
2025-09-16 11:27:07 +01:00
snipe
d1207444db Fixed nesting in orWhere 2025-09-16 11:20:49 +01:00
snipe
0bad75b263 Fixed declined asset name 2025-09-15 20:56:57 +01:00
snipe
74b98083e2 Override the getEula() method at the SnipeModel level 2025-09-15 20:05:35 +01:00
snipe
9034b5ec11 Slightly nicer display 2025-09-15 20:04:17 +01:00
snipe
927f557672 Fixed sorting on updated/created at 2025-09-15 18:13:46 +01:00
snipe
86fb089901 Remove unused blades 2025-09-15 18:13:32 +01:00
snipe
630ea05e17 A little more cleanup 2025-09-15 17:16:05 +01:00
snipe
7df5196083 A little cleanup 2025-09-15 16:09:49 +01:00
snipe
01f7b5d709 Added CJK and Arabic font support 2025-09-15 14:46:11 +01:00
snipe
ec47ee3573 Merge pull request #17850 from Godmartinz/change-purchase-cost-to-unit
Rewords Purchase cost to Unit Cost for Accessories, Components, Consumables
2025-09-15 14:20:04 +01:00
snipe
7062962cc8 Show warnings on the dates if expired or terminated 2025-09-15 13:43:08 +01:00
snipe
fde447846a Merge pull request #17865 from grokability/show-inactive-licenses
Show inactive licenses
2025-09-15 13:42:22 +01:00
snipe
319cb1bd1e Removed logging 2025-09-15 13:23:44 +01:00
snipe
58cda5ae6d Added scopes 2025-09-15 13:22:25 +01:00
snipe
251a3db880 Fixed factory 2025-09-15 13:20:34 +01:00
snipe
30b6dcd767 Added status to breadcrumbs 2025-09-15 13:13:50 +01:00
snipe
05f6622912 Added status to URL 2025-09-15 13:13:32 +01:00
snipe
36183ac19d Fixed url and tooltip text 2025-09-15 13:13:18 +01:00
snipe
f6a823e0a8 Escape text 2025-09-15 13:13:04 +01:00
snipe
312353551d Changed button color 2025-09-15 13:11:58 +01:00
snipe
fd5c9cee38 Merge pull request #17863 from grokability/added-status-label-to-asset-view
Added status label to asset view
2025-09-15 10:31:54 +01:00
snipe
84bf71802c Added status label to asset view 2025-09-15 10:31:06 +01:00
snipe
35739c2eef Merge pull request #17835 from marcusmoore/feature/10052-assigned-assets-via-api
Fixed #10052 - Added api endpoint for retrieving assets checked out to asset
2025-09-15 08:38:35 +01:00
snipe
1914a71623 Merge pull request #17851 from marcusmoore/fixes/qty-in-component-email
Fixed potentially incorrect qty in component checkout email
2025-09-15 08:37:48 +01:00
snipe
dcc53886d9 Merge pull request #17857 from uberbrady/fix_client_tls_ldap
Fixed #17414 - client-side TLS certificate didn't work in Google LDAP
2025-09-15 08:37:14 +01:00
Brady Wetherington
21ef87ef09 Merge branch 'develop' into fix_client_tls_ldap 2025-09-12 18:52:43 +01:00
snipe
b67f808da9 Fixed fieldname 2025-09-12 18:07:40 +01:00
snipe
ad69447b53 Merge pull request #17858 from grokability/ignore-expiring-licenses-with-past-termination-date
Ignore expiring licenses with past termination date
2025-09-12 17:55:14 +01:00
snipe
b4614df88c Removed awkward period 2025-09-12 17:39:52 +01:00
snipe
7171247cdc Updated tests 2025-09-12 17:37:36 +01:00
snipe
6d0084f108 Nicer alert layout for expiring asset report 2025-09-12 17:37:27 +01:00
snipe
29359f42ae Added table printout 2025-09-12 17:37:12 +01:00
snipe
cf875bf872 Changed verbiage 2025-09-12 17:37:03 +01:00
snipe
13c0d335d3 Refactor alert query 2025-09-12 17:36:51 +01:00
snipe
ceb33409b5 Removed duplicate array key 2025-09-12 17:36:33 +01:00
Brady Wetherington
83597d4a8b Possible fix to client-side TLS certificate issue for google LDAP 2025-09-12 17:12:35 +01:00
Marcus Moore
81eefc5448 Merge branch 'develop' into fixes/qty-in-component-email 2025-09-11 14:14:42 -07:00
Marcus Moore
082bc3ece4 Use correct qty in component checkout email 2025-09-11 14:10:31 -07:00
snipe
b2406b61fb Merge pull request #17826 from marcusmoore/fixes/17585-accessory-checkout-amount-in-subject
Fixed #17585 - Display accessory checkout qty in subject line and intro text
2025-09-11 21:55:20 +01:00
Marcus Moore
15698d7694 Update introduction lines with qty 2025-09-11 13:50:48 -07:00
Godfrey M
990cd82f97 change purchase cost to unit cost in several places 2025-09-11 10:46:20 -07:00
snipe
c87829b3e8 Merge pull request #17849 from Godmartinz/rollbar-unreassignablecount-fix
Fixed typo in UnreassignableCount
2025-09-11 18:43:07 +01:00
Godfrey M
6799c41d65 fixed typo 2025-09-11 10:23:53 -07:00
snipe
80c059be58 Skip terminated licenses in alert 2025-09-11 13:14:53 +01:00
snipe
aa3f896538 Merge pull request #17842 from Godmartinz/expiration-and-termination-license-enhancement
Adds #8799 Prevention of checkout of expired or terminated licenses
2025-09-11 11:46:15 +01:00
snipe
d8c17a8a5e One more small tweak to language 2025-09-11 11:40:15 +01:00
Godfrey M
850939367c adds translation warning message 2025-09-10 12:59:35 -07:00
Marcus Moore
bf4fef9bf7 Merge branch 'develop' into fixes/17585-accessory-checkout-amount-in-subject 2025-09-10 12:49:41 -07:00
Godfrey M
d5175961a4 adds a seperate formatter for checking out on license index, fix interactions 2025-09-10 12:42:10 -07:00
snipe
e7e1d6a232 Small tweaks to language 2025-09-10 20:17:33 +01:00
snipe
712345f3a0 Added link to discussions and discord 2025-09-10 20:14:19 +01:00
snipe
54c9bc3dcb Merge pull request #17840 from marcusmoore/issue-template
Made install method required in issue template
2025-09-10 20:14:10 +01:00
Godfrey M
e796c0da4a disallows checkout of expired or terminated licenses 2025-09-10 12:08:14 -07:00
Marcus Moore
cf8ff0f43e Add note on English 2025-09-10 10:41:20 -07:00
Marcus Moore
e67ce23a7c Require installation method 2025-09-10 10:23:22 -07:00
snipe
a66bb95a81 A few more tweaks 2025-09-10 15:48:21 +01:00
snipe
c66fa33b2e Have I mentioned how much I hate YAML? 2025-09-10 15:43:09 +01:00
snipe
56eebb9db4 Small tweaks to templates 2025-09-10 15:40:12 +01:00
snipe
d9773f107e Added feature request form
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:38:19 +01:00
snipe
e09112f46a Require installation type
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:33:06 +01:00
snipe
50a17a82b6 Sigh
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:30:42 +01:00
snipe
7eb032d646 Rename issue template (I guess?)
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:25:51 +01:00
snipe
e065f22f8e Merge pull request #17838 from grokability/issue-form
New GH issue form
2025-09-10 15:20:51 +01:00
snipe
c1b4ba1f85 Last one for now
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:20:27 +01:00
snipe
eeaec471f0 Dunno if this will work
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:19:29 +01:00
snipe
0d3c8678d8 Moved text
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:17:16 +01:00
snipe
bbddf5f95b Still a few more tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:15:09 +01:00
snipe
3b8c8b3af9 Nicer markdown
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:12:38 +01:00
snipe
84753aa13f Added browser console
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:08:37 +01:00
snipe
90b84451d8 Fixed indenting
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:07:04 +01:00
snipe
54c8ae41cc Still hate YAML
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:06:03 +01:00
snipe
7d32b1a724 God I hate YAML
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:05:27 +01:00
snipe
69ffd63ca6 Removed backticks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:02:50 +01:00
snipe
4857c19eb6 More tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:02:07 +01:00
snipe
d535e23da0 More template tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 14:51:33 +01:00
snipe
30e02544ab Try renaming so I can preview
via https://github.com/orgs/community/discussions/7039#discussioncomment-5327083

Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 14:43:16 +01:00
snipe
ee53925bd2 Starter for preview
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 14:40:38 +01:00
snipe
40495b8a17 Small demo tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 12:33:33 +01:00
Marcus Moore
6bc9a82a7a Formatting 2025-09-09 14:51:22 -07:00
Marcus Moore
6504ee37bd Remove dump 2025-09-09 13:09:52 -07:00
Marcus Moore
082bff2fa8 Add and use query scope helper 2025-09-09 13:04:14 -07:00
Marcus Moore
ab7bd86336 Improve assertions 2025-09-09 13:01:59 -07:00
Marcus Moore
eada0b0bb5 Implement test 2025-09-09 11:58:43 -07:00
Marcus Moore
f221f9f22a Implement test 2025-09-09 11:50:41 -07:00
snipe
6731e44a0d Merge pull request #17828 from marcusmoore/fixes/17586-acceptance-banner-qty
Fixed #17586 - Display accurate quantity in banner
2025-09-09 19:38:34 +01:00
Marcus Moore
acc37045e4 Implement test 2025-09-09 11:34:50 -07:00
Marcus Moore
a7c5899c16 Implement test 2025-09-09 11:33:54 -07:00
Marcus Moore
80b02635a9 Add test stub 2025-09-09 10:10:53 -07:00
snipe
f90de5ec67 Merge pull request #17833 from grokability/tighter-controls-on-licenses
Tighter controls on license deletion, also fixes #16227 adding more tables to location print
2025-09-09 15:35:12 +01:00
snipe
9a3e046530 Fixed HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:30:34 +01:00
snipe
7f56e461fe Added child location icon
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:19:00 +01:00
snipe
1da37e0d38 Added child location count
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:18:47 +01:00
snipe
0004d4936c Cleaned up print view
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:18:28 +01:00
snipe
7a6fdc4e0a Added parent ID to location API
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:18:09 +01:00
snipe
2eb727bd0c Added tests
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 13:51:56 +01:00
snipe
57af507170 Added deleted button to locations, check for additional relations
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 12:20:34 +01:00
snipe
e37f87465c Merge pull request #17827 from Godmartinz/duplicate-emails-fix
Fixed #17756 - Duplicate checkout emails
2025-09-09 09:33:03 +01:00
Marcus Moore
324070f345 Begin to build out test 2025-09-08 17:20:22 -07:00
Marcus Moore
e1aa843b6d Scaffold test 2025-09-08 17:05:14 -07:00
Godfrey M
e652a7fd61 fix tests 2025-09-08 14:56:15 -07:00
Marcus Moore
2397bfbad0 Improve variable name 2025-09-08 14:51:19 -07:00
Marcus Moore
7e2bc8e452 Display accurate quantity in banner 2025-09-08 14:48:11 -07:00
Godfrey M
00e8fd0483 cloned mailable 2025-09-08 14:44:58 -07:00
Marcus Moore
6d8bf2c665 Display accessory checkout qty in subject line 2025-09-08 14:25:56 -07:00
snipe
72466f1aab Revert "Merge pull request #17823 from grokability/#17822-fix-n+1-in-topmenu"
This reverts commit 6901deccbf, reversing
changes made to 6b87c90e02.

Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 15:04:28 +01:00
snipe
6901deccbf Merge pull request #17823 from grokability/#17822-fix-n+1-in-topmenu
Fixed #17822 - n+1 in top menu check
2025-09-08 14:33:47 +01:00
snipe
5a9c906eb9 Added filter to assets loading
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 14:32:58 +01:00
snipe
b95b60b49e Use eager-loaded model assets
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 14:07:49 +01:00
snipe
14408ef18f Fixed n+1 in top menu check
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 13:55:22 +01:00
snipe
6b87c90e02 Use scope for assets for show in sidebar
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 13:33:19 +01:00
snipe
2b4d5222eb Made “add new” buttons clearer on table headers
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 12:17:29 +01:00
snipe
9604ecebad Fixed jobtitle in advanced search
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 12:01:59 +01:00
snipe
0d67970a45 Update @swift2512 as a contributor 2025-09-08 09:27:34 +01:00
snipe
913b9f0c40 Reworks PR #15490 - adds location to inventory email
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 09:26:36 +01:00
snipe
610a5745f0 Merge pull request #17818 from grokability/dependabot/github_actions/develop/actions/stale-10
Bump actions/stale from 9 to 10
2025-09-08 09:17:19 +01:00
dependabot[bot]
dff12324c6 Bump actions/stale from 9 to 10
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9...v10)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '10'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 08:16:24 +00:00
snipe
f340390fc8 Merge pull request #17703 from marcusmoore/17369-accessory-checkout-qty-scaffold
Fixed issues around accessory acceptance
2025-09-08 09:09:26 +01:00
snipe
be81e74921 Shifted log meta position
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 08:24:50 +01:00
snipe
2bee8729e4 Better escaping for display_name in API controller
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 08:14:33 +01:00
snipe
5d03038734 Use auto-direction for <p> in preview
Signed-off-by: snipe <snipe@snipe.net>
2025-09-05 15:12:38 +01:00
snipe
75b11de0f4 Bumped composer packages
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 19:55:30 +01:00
snipe
c5bede8594 More small display_name fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 17:15:06 +01:00
snipe
cd9ea6ae3b Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:56:43 +01:00
snipe
113b762ec7 Fix for null location in locations print
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:55:44 +01:00
snipe
78704d8b85 Fixed #17803 - checked out to name in custom report
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:51:10 +01:00
snipe
1109db76fe Merge pull request #17797 from grokability/#17796-search-on-model-name-and-number
Fixed #17796 - search on model name and number on importer
2025-09-04 16:35:43 +01:00
snipe
b1b390febf Removed flaky test
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:30:02 +01:00
snipe
be451fa0c0 Removed asset model from original factory
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:14:27 +01:00
snipe
1fa553c785 Use nths
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:04:16 +01:00
snipe
905f61371d Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:02:40 +01:00
snipe
7da5210a01 Switched to nth from fetchOne in CSV reader
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 12:50:12 +01:00
snipe
18172d3896 Merge pull request #17752 from marcusmoore/fixes/existing-assets-report
Improve expiring alerts notification layout
2025-09-04 12:33:32 +01:00
snipe
c28e78b9e2 Merge pull request #16063 from Godmartinz/checkin_non_reassignable_license
Allows check-ins of unreassignable licenses
2025-09-04 11:22:17 +01:00
snipe
e7827a3847 Merge pull request #17800 from marcusmoore/chore/test-action-logs
Upload log file in GitHub Action tests
2025-09-04 10:20:12 +01:00
Marcus Moore
039564e74c Wrap migration in check 2025-09-03 12:52:46 -07:00
Marcus Moore
e164595a0f Fall back to displaying 1 2025-09-03 12:35:18 -07:00
Marcus Moore
d29e09a3ff Merge branch 'develop' into 17369-accessory-checkout-qty-scaffold
# Conflicts:
#	resources/views/account/accept/create.blade.php
#	resources/views/account/accept/index.blade.php
2025-09-03 12:26:43 -07:00
Godfrey M
db9f85e9da Merge branch 'develop' into checkin_non_reassignable_license
# Conflicts:
#	app/Models/License.php
#	resources/views/licenses/view.blade.php
#	tests/Feature/Checkins/Api/LicenseCheckInTest.php
2025-09-03 11:09:51 -07:00
Marcus Moore
27022954b1 Inline icon 2025-09-03 10:44:19 -07:00
Marcus Moore
30362c924f Upload log as artifact 2025-09-03 10:13:01 -07:00
snipe
bf63b15b46 Merge pull request #17799 from grokability/#17798-adds-require-serial-to-importer
Fixed #17798 - added `require_serial` to model importer
2025-09-03 15:32:42 +01:00
snipe
19aea4bd6c Fixed #17798 - added require_serial to model importer
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:25:56 +01:00
snipe
090890e9c6 Fixed #17796 - search on model name and number on importer
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:15:29 +01:00
snipe
605022a9e3 Merge pull request #17795 from grokability/#17791-larger-currency-field
Fixed #17791 - increase size of purchase cost field
2025-09-03 15:09:28 +01:00
snipe
b06c58fe7b Switch to older style rules for consistency
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:06:27 +01:00
snipe
f5c8b3eb04 Fixed #17791 - increase size of purchase cost field
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:03:49 +01:00
snipe
739980aa09 Merge pull request #16947 from Godmartinz/add-require-serial-to-models
Adds require serial as Asset Model option
2025-09-03 14:53:15 +01:00
snipe
afde5943e3 Fixed typo - #17784
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 14:39:59 +01:00
snipe
32300cb42c Merge pull request #17788 from grokability/add-delete-log-instead-of-soft-deleting-the-log-itself
Fixed #17777 - Log upload deletion
2025-09-03 14:37:32 +01:00
snipe
de3b1697c8 Merge pull request #17760 from Godmartinz/fix-translation-string-in-notifications
Fixes #17759 translation used in asset check in/out notifications
2025-09-03 14:33:57 +01:00
snipe
a18fb10b5a Merge pull request #17783 from marcusmoore/fixes/company-in-location-print
Fixed company name reference in location print
2025-09-03 08:44:40 +01:00
snipe
52140dbe06 Log upload deletion
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 08:37:42 +01:00
Marcus Moore
db5bb1928e Merge branch 'develop' into fixes/existing-assets-report
# Conflicts:
#	resources/views/notifications/markdown/report-expiring-assets.blade.php
2025-09-02 15:07:23 -07:00
Marcus Moore
65b66beb07 Make icon more prominent 2025-09-02 14:55:46 -07:00
Marcus Moore
c83504b4e7 Use display_name in place of presenter 2025-09-02 12:57:40 -07:00
Godfrey M
cd2e7ee31d fix google and slack notifications 2025-09-02 10:53:42 -07:00
Godfrey M
c3a0a0415a fix MS Teams Notifications 2025-09-02 10:48:02 -07:00
snipe
709f4672b7 Merge pull request #17771 from grokability/#10107-remember-checkout-to-type
Fixed #10107 - remember checkout to type
2025-09-02 13:46:13 +01:00
snipe
e6c030b050 Merge pull request #17781 from grokability/#17780-add-withtrashed-to-files
Fixed #17780 - Added `withTrashed()` to allow viewing files on deleted objects
2025-09-02 13:45:18 +01:00
snipe
7bd3a791a1 Added withTrashed() to allow viewing files on deleted objects
Signed-off-by: snipe <snipe@snipe.net>
2025-09-02 13:39:35 +01:00
snipe
b9cfc03b4f display name fix
Signed-off-by: snipe <snipe@snipe.net>
2025-09-02 11:30:34 +01:00
snipe
131327a64d Fixed “undefined” error on status labels in BS tables
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 20:32:42 +01:00
snipe
77d002a158 Use null for role as well
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:43:05 +01:00
snipe
94699893ac Updated fieldname in user API request
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:17:39 +01:00
snipe
9f81989bdd Return null instead of blank for display_name in API
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:06:28 +01:00
snipe
15abe36c53 More tweaks to display
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:58:02 +01:00
snipe
3094e007ee Set session to remember checkout type
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:28:07 +01:00
snipe
eb259aee22 Set asset selector to true for components since it will always be required
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:18:58 +01:00
snipe
c05c8defb9 Merge pull request #17769 from grokability/#9978-add-pivot-to-accessories-api
Fixes #9978 - Added payload to accessories API
2025-09-01 12:28:08 +01:00
snipe
bf5668a42e Added payload to accessories API
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 12:23:39 +01:00
snipe
ec310bc8fb Updated dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 11:56:01 +01:00
snipe
db477421b2 Bumped to 8.3.1
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 11:54:53 +01:00
snipe
30a9496cf5 Merge pull request #17748 from Godmartinz/parent-location-in-asset-view
Adds #10969 Parent location to Asset Checked out to info
2025-09-01 11:49:42 +01:00
snipe
6cefa0d0b3 Merge pull request #17745 from Godmartinz/dropdown-link-color-fix
Fixes #17488 (Part 2) Nav dropdown link color and skin names
2025-09-01 11:48:29 +01:00
snipe
9284984265 Merge pull request #17768 from Speedyduck300000/develop
Fixed #17742: don't delete random actionlog when trying to delete a file
2025-09-01 11:40:06 +01:00
Speedyduck300000
53b96168a9 fixed uploadfilescontroller to use the file_id to delete the correct
entry.
2025-09-01 07:34:18 +02:00
Godfrey M
eadce51f10 fixes check in check out translations for assets in notiications 2025-08-29 11:12:05 -07:00
snipe
b3c583b6dc Few more ->display_name
Signed-off-by: snipe <snipe@snipe.net>
2025-08-29 10:17:16 +01:00
snipe
28abeab31d Fixed a few more display_name instances
Signed-off-by: snipe <snipe@snipe.net>
2025-08-29 10:06:58 +01:00
snipe
12a649ec4b Merge pull request #17751 from marcusmoore/fixes/name-in-expiring-assets
Fixed user display name in expiring asset notification
2025-08-29 01:11:05 +01:00
Marcus Moore
35b79e4d14 Remove old comments 2025-08-28 16:56:40 -07:00
Marcus Moore
751dad7f2e Inline days 2025-08-28 16:56:12 -07:00
Marcus Moore
b08d86220a First pass at moving to table structure 2025-08-28 16:53:13 -07:00
Marcus Moore
3a27ecc475 Use display_name in place of name() 2025-08-28 16:22:24 -07:00
Godfrey M
da6fab5d43 adds parent location to checked out to location 2025-08-28 13:37:40 -07:00
snipe
ca95b29cd6 Add telescope tables to the excepted tables
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 19:52:56 +01:00
Godfrey M
c5c68e9dd5 fix dropdown link color and skin names 2025-08-28 11:49:37 -07:00
snipe
44fbde26fa Merge pull request #17743 from grokability/normalize-trait-locations
Moved model traits into proper directory
2025-08-28 18:52:43 +01:00
snipe
6e2bcd6aa9 Merge pull request #17680 from grokability/added-telescope
Added laravel telescope for dev environment
2025-08-28 18:52:18 +01:00
snipe
9c0202e5ce Bumped to 8.3.0
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 18:33:46 +01:00
snipe
39ef353073 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 18:32:07 +01:00
snipe
7b5d90dd81 Moved model traits into proper directory
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 18:23:26 +01:00
snipe
d1129081df Asset nothing is sent if send_welcome is not checked/passed
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 18:05:40 +01:00
snipe
315a812df5 Fixed typos
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 16:41:56 +01:00
snipe
cfc979acf0 Merge pull request #17432 from oolivero45/patch-1
Fixed #17431: EULA not displaying on asset acceptance page
2025-08-28 16:09:36 +01:00
snipe
d7407d70a3 Merge pull request #17741 from grokability/improved-user-create-tests
Improved user create tests
2025-08-28 13:32:46 +01:00
snipe
8ccd2e97a8 Improved user create tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 13:27:08 +01:00
snipe
988204619f Merge pull request #17740 from grokability/renamed-user-test
Renamed the test for consistency
2025-08-28 13:05:05 +01:00
snipe
cad6cc3007 Renamed the test for consistency
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 13:02:01 +01:00
snipe
b303875f1d Merge pull request #17734 from grokability/#17726-add-welcome-email-to-new-user-form
Fixed #17726: add welcome email to new user form
2025-08-28 07:29:56 +01:00
snipe
d5cc61f378 Added send to API call for creating users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 07:28:51 +01:00
snipe
0d7ec43262 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:19:41 +01:00
snipe
d3747f4daa Added welcome email to controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:12:01 +01:00
snipe
af695e7dc8 Added help to user importer
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:52 +01:00
snipe
1edbfd87df Added welcome email checkbox to user create form
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:40 +01:00
snipe
454be01a6c Updated translations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:23 +01:00
snipe
745fc515f1 Merge pull request #17713 from Godmartinz/fix-localization-for-email-notifications
Adds #5554 locale for acceptance notifications and checkin/out emails
2025-08-28 05:29:28 +01:00
snipe
715b9c1182 Merge pull request #17730 from Godmartinz/update-asset-accepance-with-category
Adds #9000 Item type to Account Asset Acceptance index
2025-08-28 05:22:54 +01:00
Godfrey M
95be847d87 renamed attribute 2025-08-27 14:10:51 -07:00
Godfrey M
c1a6546eba change column header 2025-08-27 14:09:13 -07:00
Godfrey M
648c25a0a7 adds item type to Accept asset index 2025-08-27 14:06:10 -07:00
Godfrey M
f2ec7f2975 fix tests 2025-08-27 13:22:35 -07:00
Godfrey M
f518af6d61 fix class name 2025-08-27 13:09:05 -07:00
snipe
b11c6a5c06 Updated depreciation translation with more information.
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 16:35:49 +01:00
snipe
5822e4e692 Merge pull request #17729 from grokability/exit-early-if-ldap-troubleshooter-cannot-decrypt-ldap-pw
Put LDAP troubleshooter's decrypt in a try/catch to avoid crashing if it cannot decrypt the password
2025-08-27 15:47:22 +01:00
snipe
e4f06b0ca8 One last time
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 15:43:48 +01:00
snipe
2f093c0e82 Added early exist on step 4 as well
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 15:41:39 +01:00
snipe
5d9dc0e74d Put decrypt in a try/catch
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 15:33:26 +01:00
snipe
adc3a34929 Fixed copy for encrypted custom fields
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 14:38:36 +01:00
snipe
cb2ffe6b3f Updated translations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 14:02:39 +01:00
snipe
b3e3d01672 Fixed LDAP icon spacing
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 13:32:02 +01:00
snipe
4a6520fc78 Fixed address field
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 13:30:07 +01:00
snipe
75ab6c9b13 Merge pull request #17723 from uberbrady/improve_ldap_certificate_ignoring
Improve ldap certificate ignoring
2025-08-27 13:29:33 +01:00
snipe
2f77fcb526 Merge pull request #17724 from Godmartinz/checkout2location_email_fix
Fixes #17642 Checkouts to location email for Assets and Accessories
2025-08-27 13:02:57 +01:00
Brady Wetherington
60604c3481 With the new SSL stuff, we are calling ldap_set_option() one more time now 2025-08-27 12:25:39 +01:00
Godfrey M
671c113cd2 add coma to translation" 2025-08-26 16:07:04 -07:00
Godfrey M
8a74d21ede fixes checkout emails to location for assets and accessories" 2025-08-26 16:00:26 -07:00
Godfrey M
75995b2109 fix checkout to location email 2025-08-26 15:34:38 -07:00
Marcus Moore
4298aad008 Merge branch 'develop' into 17369-accessory-checkout-qty-scaffold
# Conflicts:
#	app/Http/Controllers/Account/AcceptanceController.php
#	resources/views/notifications/markdown/asset-acceptance.blade.php
2025-08-26 11:39:52 -07:00
Marcus Moore
823c67400d Fix indent 2025-08-26 11:34:10 -07:00
Marcus Moore
3160d1064d Remove unused import 2025-08-26 11:32:38 -07:00
snipe
d1eefc3fea Merge pull request #17692 from grokability/#17387-make-saml-key-size-an-env
Fixed #17386 - Added SAML key size to env - possible alternative to #17387
2025-08-26 16:28:27 +01:00
Brady Wetherington
16795382fc Many cleanups to default-mode of LDAP troubleshooter 2025-08-26 15:53:18 +01:00
snipe
eb17974adc Merge pull request #17722 from grokability/#17704-retain-linebreaks
Fixed #17704 -  retain linebreaks on custom field clipboard copy
2025-08-26 15:44:38 +01:00
snipe
22852c27f8 Use generic length for asterisks
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:38:56 +01:00
snipe
f4a94d975d Fixes #17704 - retain linebreaks in clipboard for multi-line custom field copying
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:33:19 +01:00
snipe
7a36bbbd1e Merge pull request #17721 from grokability/small-ldap-preview-display-tweaks
Improved LDAP field sync preview
2025-08-26 15:26:09 +01:00
snipe
2b401b965b Fixed casing
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:22:00 +01:00
snipe
314bc5b44f Added manager
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:14:29 +01:00
snipe
76374f0d5a Updated text
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:14:22 +01:00
snipe
264efb015e Fixed jobtitle field mapping
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:05:05 +01:00
Brady Wetherington
e74460aefc Merge branch 'develop' into improve_ldap_certifcate_ignoring 2025-08-26 15:01:11 +01:00
Brady Wetherington
55a5a12b30 Formalize the 'double-barrel' method of setting TLS cert ignores 2025-08-26 15:00:33 +01:00
snipe
58944a38eb Make screen and table wider
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 14:59:11 +01:00
snipe
469e3bd475 Nicer ldap preview layout, show all mapped fields
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 14:51:34 +01:00
snipe
17650c5735 Changed field title
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 14:03:11 +01:00
Brady Wetherington
15e64155b5 Add version checking to LDAP troubleshooter, clean up ldap model 2025-08-26 13:57:25 +01:00
snipe
39955ac760 Add @akaspeh1 as a contributor 2025-08-26 12:42:20 +01:00
snipe
855a176ca9 Add @nickwest as a contributor 2025-08-26 12:42:15 +01:00
snipe
47b2b30455 Merge pull request #17710 from akaspeh1/develop
Adds support for label sheets Avery L4736 & L6009
2025-08-26 12:42:02 +01:00
snipe
b702e3e2de Merge pull request #17492 from ischooluw/17448-feature-notes-api-endpoints
Fixes #17448: feat(api) - API endpoint for Adding Ad-Hoc Notes to Assets
2025-08-26 12:40:52 +01:00
snipe
a6b74d56c6 Merge pull request #17709 from grokability/add-display-name-to-users-fixed
Added display name to users for LDAP/SCIM, added new sync fields (replaced #17650)
2025-08-26 12:39:25 +01:00
snipe
a4222bcaef Merge pull request #17711 from grokability/dependabot/github_actions/develop/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-26 12:10:24 +01:00
snipe
ecf24511cd Fixed tests for real this time tho
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 12:09:55 +01:00
snipe
abb097a391 Merge pull request #17714 from Godmartinz/Audit_null_fix
Added null checks to MS Teams Audit notification
2025-08-26 10:44:51 +01:00
Godfrey M
dd742a2e4a add a check for audit notification variables in MS Teams and a translation 2025-08-25 15:10:41 -07:00
Godfrey M
128bdf500a sends an email for to locale and cc locale 2025-08-25 12:02:23 -07:00
dependabot[bot]
73ac00bc51 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 16:25:39 +00:00
snipe
3524e23e38 Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 17:17:45 +01:00
Jakub Aqaba Štarman
be0f3910bb Fixed: Old computation 2025-08-25 16:57:32 +02:00
snipe
07dbc6842c Are you KIDDING ME, Github??
This reverts commit c8e79aa5ca, reversing
changes made to e60f2b2332.

Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 15:56:28 +01:00
Jakub Aqaba Štarman
5a16b59462 Adds support for label sheets Avery L4736 & L6009 2025-08-25 16:47:52 +02:00
Brady Wetherington
13cd7071b8 WIP improving some LDAP stuff 2025-08-25 15:41:01 +01:00
snipe
40108b196c Trying to fix import tests :(
Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 15:28:43 +01:00
snipe
c8e79aa5ca Merge branch 'develop' into add-display-name-to-users-fixed 2025-08-25 15:28:20 +01:00
snipe
e60f2b2332 Tightened up accessor code for better inheritence
Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 15:00:10 +01:00
snipe
b6d397bcca Updated ->present()->fullName() to ->display_name
Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 14:57:34 +01:00
Marcus Moore
918426a2fa Add qty to accept assets table 2025-08-21 15:49:36 -07:00
Marcus Moore
c076b37c9b Add qty to accessory eula pdf 2025-08-21 15:05:24 -07:00
Marcus Moore
7c58bfa282 Add qty to AcceptanceAssetDeclinedNotification 2025-08-21 14:56:24 -07:00
Marcus Moore
0caaba156d Add qty to AcceptanceAssetAcceptedNotification 2025-08-21 14:54:43 -07:00
Marcus Moore
c22575812d Add qty to AcceptanceAssetAcceptedToUserNotification 2025-08-21 14:53:00 -07:00
Marcus Moore
0ede4da816 Remove CleanDeclinedAccessoryCheckouts command 2025-08-21 14:34:31 -07:00
Marcus Moore
98e23ff92e Remove legacy testing from test 2025-08-21 14:34:16 -07:00
snipe
6503f9c667 Revert "Merge pull request #17650 from grokability/add-displayName-to-users"
This reverts commit 4770e469b4, reversing
changes made to 29a18c7c8b.

Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 20:23:47 +01:00
snipe
4770e469b4 Merge pull request #17650 from grokability/add-displayName-to-users
Add display name to users for LDAP/SCIM, added new sync fields
2025-08-21 18:22:34 +01:00
snipe
29a18c7c8b Merge pull request #17696 from uberbrady/add_created_at_index_to_models
Fixed [FD-49550] - added a 'created_at' index to the models table
2025-08-21 14:54:20 +01:00
Brady Wetherington
6db0003e3f Adds a 'created_at' index to the models table 2025-08-21 13:44:14 +01:00
snipe
c538c460fa Merge pull request #17695 from grokability/#17482-better-localization-indates-on-asset-view
Use nicer local for purchase date
2025-08-21 13:13:26 +01:00
snipe
822339fe42 Moved warning
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 13:13:11 +01:00
snipe
b84d9282ca Use normal locale for warranty
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 13:05:01 +01:00
snipe
952b6f33bb Add @strobelm as a contributor 2025-08-21 11:51:37 +01:00
snipe
c57c4b8ff2 Merge pull request #17691 from qay21/fix-components-url
Fix components presenting wrong URLs
2025-08-21 11:37:27 +01:00
snipe
39e6223ff2 POssible alternative to #17386 - adding SAML key size to env
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 11:27:50 +01:00
qay
d8dd274c08 Fix components presenting wrong URLs 2025-08-21 12:26:13 +02:00
snipe
15f97b6cb9 Merge pull request #17591 from Godmartinz/add-serial-to-expiring-asset-report
Adds #17440 serial number column to Expiring Assets Report
2025-08-21 11:14:45 +01:00
snipe
fc091c1174 Added comments
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 09:29:12 +01:00
snipe
c07ef4d87f A few small tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 09:25:42 +01:00
Marcus Moore
c7bdad649a Build out command 2025-08-20 16:06:11 -07:00
Marcus Moore
18c2508d2f Scaffold command for cleaning accessory_checkout 2025-08-20 13:49:19 -07:00
Marcus Moore
4dcfd8b353 Improve method name 2025-08-20 13:19:00 -07:00
Marcus Moore
726116574d Improve readability? 2025-08-20 12:40:03 -07:00
Marcus Moore
27f02014ca Add failing test 2025-08-20 12:26:55 -07:00
Marcus Moore
f80f1acaa7 Improve variable name 2025-08-20 12:15:58 -07:00
Marcus Moore
39d5ffeceb Implement fix 2025-08-20 12:05:58 -07:00
Marcus Moore
48ba7eed3e Remove legacy checks from test 2025-08-20 11:45:36 -07:00
snipe
11eee833bb Fixed #17667 - Switch to hyphens for windows
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 15:56:10 +01:00
snipe
35b358d336 Check for $user to handle tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:47:58 +01:00
snipe
ae109be631 Small tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:43:52 +01:00
snipe
3f7ed73395 Added laravel telescope for dev environment
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:26:28 +01:00
snipe
fec9d716ee Merge pull request #17679 from grokability/#17674-add-ods-and-odt
Fixed #17674: added .ods, .odp, and .odt as acceptable upload types
2025-08-20 14:17:08 +01:00
snipe
da5b1afd19 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:11:42 +01:00
snipe
618106c103 Fixed #17674 - added odp, ods, odt to accepted files
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:11:20 +01:00
snipe
312be98132 Add @FlorestanII as a contributor 2025-08-20 12:43:43 +01:00
snipe
e0bb77a6d6 Merge pull request #17664 from FlorestanII/feature/support-for-dymo-11354-labels
Support for Dymo 11354 Labels.
2025-08-20 12:43:29 +01:00
snipe
855922c21a Account for null in tetss (vs 0)
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 11:32:16 +01:00
snipe
bc645d2621 Use email formatter in licensed_to_email display
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 11:24:16 +01:00
snipe
9c06ff3899 Check for numeric
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 11:00:18 +01:00
snipe
2a37aa3b49 Fixed tooltip
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 10:34:05 +01:00
snipe
bf591320af Fixed #17665 - delete custom report modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 09:58:30 +01:00
snipe
56e687bed2 Retuen the display name in the API call
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 09:33:00 +01:00
Marcus Moore
9caa240fdb Revert "Remove total qty of accessory checkouts"
This reverts commit 3ffb73a516.
2025-08-19 17:17:46 -07:00
Marcus Moore
3ffb73a516 Remove total qty of accessory checkouts 2025-08-19 17:04:13 -07:00
Marcus Moore
cc1132be87 Remove flakiness 2025-08-19 16:50:36 -07:00
Marcus Moore
1c31f126ef Clear some flakiness 2025-08-19 16:46:41 -07:00
Marcus Moore
d8eaf2676f Add a couple of notes 2025-08-19 15:54:46 -07:00
snipe
07b25fe376 Add display name to summary
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 20:52:18 +01:00
snipe
c2ecd20b7d Updated field text
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 20:47:48 +01:00
snipe
1b42abcc98 Fixed mapping
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 19:54:32 +01:00
snipe
9efb49d510 Merge pull request #17663 from Godmartinz/sub-out-translation
Fixes #17653 changes translation to administrator
2025-08-19 19:43:47 +01:00
snipe
2d6270c697 Updated validation, switch to string() as db field type
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 19:19:29 +01:00
snipe
0823c23a6e Fixed placeholder text
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 18:51:56 +01:00
snipe
b3f0ce4b2a Use fieldsets for LDAP settings
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 18:38:47 +01:00
snipe
8b83584b67 Added mapping fields to LDAP
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 18:31:58 +01:00
Godfrey M
9eb686fe08 changes translation to administrator 2025-08-19 10:23:15 -07:00
Johannes Pollitt
765051ce88 Added LabelWriter for 11354 format labels.
Printable for example with the Dymo LabelWriter 450.
2025-08-19 19:21:48 +02:00
Godfrey M
ed402e0122 adds serial underneath name 2025-08-19 10:10:20 -07:00
snipe
1488271a83 Added #8522 - depreciation info on Asset API
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 14:48:48 +01:00
snipe
48bbf8d005 Merge pull request #17655 from uberbrady/add_category_indexes
Add new indexes to category_id and deleted_at
2025-08-19 14:26:38 +01:00
Brady Wetherington
e97b969d66 Add new indexes to category_id and deleted_at 2025-08-19 14:20:36 +01:00
snipe
cdd12df81a Fixed #17627 - jquery UI fix for draggable/sortable
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 14:12:06 +01:00
snipe
050a3afc74 Fixed #17649 - nicer layout on new location modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 13:56:21 +01:00
snipe
270401c693 Added display name to user create modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 13:12:57 +01:00
snipe
551822ce7d Merge pull request #17648 from grokability/possible-fix-for-#17641-map-mobile-via-scim
Fixed #17641: map mobile number via SCIM
2025-08-19 13:09:07 +01:00
snipe
4b8c371097 Updated true to false
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:59:28 +01:00
snipe
90fbf6da46 Modify the presenter to see if they have a display_name set
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:56:44 +01:00
snipe
0c3103e3d2 Modify the getter
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:56:30 +01:00
snipe
6a8e1566fe Added display_name to a few more places
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:56:11 +01:00
snipe
ced30082a6 Added display_name as user field
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:10:28 +01:00
snipe
f6c64abc1a Fixed #17641 - map mobile number via SCIM
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 11:41:02 +01:00
snipe
7f9939a896 Merge pull request #17638 from Godmartinz/asset-tag-added-to-subject-line
Adds asset tag to subject line of check in check out
2025-08-19 09:36:25 +01:00
Godfrey M
1c99f2dfdd readd doesntorequireacceptance() to test 2025-08-18 10:52:35 -07:00
Godfrey M
1974fccac3 add tag to other notification test 2025-08-18 10:48:39 -07:00
Godfrey M
911552035e fix other test 2025-08-18 10:39:10 -07:00
Godfrey M
ff25d275ee fix tests 2025-08-18 10:31:03 -07:00
Godfrey M
1fcf5e03e7 adds asset tag to subject line of checkin/out 2025-08-18 10:16:47 -07:00
Marcus Moore
3101212c49 Continue to scaffold test 2025-08-14 17:24:36 -07:00
Marcus Moore
3f0ac103a1 Scaffold test 2025-08-14 13:29:18 -07:00
Marcus Moore
59bd6ca360 Update button text 2025-08-14 13:21:11 -07:00
Marcus Moore
22fe9a786e Display number of items being accepted 2025-08-14 12:36:46 -07:00
Marcus Moore
d2ee8de9ac Attach qty to CheckoutAcceptance 2025-08-13 12:14:10 -07:00
Marcus Moore
03d3fb6a5f Add qty to checkout_acceptances table 2025-08-13 12:09:56 -07:00
Godfrey M
ba85af11aa adds serial to expiring assets report email 2025-08-12 14:59:20 -07:00
Nicky West
c94a8c42f4 Changed NotesController::getList() to NotesController::index() & reordered methods for consistency 2025-07-28 16:57:46 -07:00
Nicky West
16fdb16a56 Changed over to route model binding and simplified logic & gates 2025-07-28 16:55:11 -07:00
Nicky West
822f9a6f28 Fixed deviations from code standards 2025-07-28 16:37:08 -07:00
Nicky West
b264bbf69f feat(api): Add API endpoints for managing asset history notes
- Add POST endpoint to create a history note attached to an asset
- Add GET endpoint to retrieve history notes for an asset
- Add ActionLog factory state for manual notes
- Implement controller methods with authorization checks
- Add feature tests for note creation, retrieval, and access control
- Register new API routes for these endpoints

Supports automation by enabling programmatic asset history note management.
2025-07-28 15:55:37 -07:00
Oliver Cox
553ab8851a Fix #17431: EULA not displaying on asset acceptance page
Changed two instances of === to ==, as this was causing a comparison to fail and preventing EULAs from being displayed on asset acceptance pages as expected.
2025-07-20 23:00:18 +01:00
Godfrey M
2b8ea9a233 add required to input validation 2025-07-07 10:46:54 -07:00
Godfrey M
b0067fee51 remove some N+1s, collect an array of missing serial errors 2025-05-20 12:31:34 -07:00
Godfrey M
732c3dae89 added require_serial to model factory 2025-05-20 09:53:51 -07:00
Godfrey M
d45bd67cae added corrections 2025-05-20 09:51:02 -07:00
Godfrey M
9200de5032 made require_serial column nullable 2025-05-19 09:58:37 -07:00
Godfrey M
3fbbff5a47 revert unnecessary change to laels 2025-05-14 12:57:01 -07:00
Godfrey M
c22efc2c3d add to present and transformer and api 2025-05-14 12:55:41 -07:00
Godfrey M
8c0281bf70 adds tests for requiring serial to asset model 2025-05-14 12:31:27 -07:00
Godfrey M
720a4bc4a2 add warning to update method for missing a serial 2025-05-13 12:54:28 -07:00
Godfrey M
7fd93645b3 valdiation fires for asset creation 2025-05-13 12:17:58 -07:00
Godfrey M
fcbfbca6d0 add checkbox to model edit and create 2025-05-13 11:10:46 -07:00
Godfrey M
f2bca9491c changed name of field in model fillable 2025-05-13 10:59:02 -07:00
Godfrey M
b48f309ab6 add require_serial to bulk asset model blades and lang 2025-05-13 10:58:05 -07:00
Godfrey M
0b1be3e63b add migration, model and controller update 2025-05-12 11:44:34 -07:00
Godfrey M
8c129c10af removed payload from api error message 2025-04-30 11:11:53 -07:00
Godfrey M
63ce2a14fe fix the ghosts pt2 2025-04-23 10:17:11 -07:00
Godfrey M
f435ebb110 fix the ghosts 2025-04-23 10:15:26 -07:00
Godfrey M
843f001bf6 rename test class 2025-04-23 09:33:56 -07:00
Godfrey M
0d28165c04 merged develop, fix conflicts 2025-04-21 10:54:01 -07:00
Godfrey M
ee31bfbcd4 Merge branch 'develop' into checkin_non_reassignable_license
# Conflicts:
#	app/Helpers/Helper.php
#	app/Http/Controllers/Api/LicenseSeatsController.php
#	app/Http/Transformers/LicensesTransformer.php
2025-04-21 10:49:35 -07:00
Godfrey M
1c67d6802d added testing to Api check in, renamed other test method 2025-01-27 12:18:24 -08:00
Godfrey M
5da8c86ec7 fix license query for bulk licenses checkin for assets 2025-01-22 10:55:41 -08:00
Godfrey Martinez
10a2d59ec1 Merge pull request #27 from Godmartinz/checkin_non_reassignable_license_cleanup
adds bulk check in of unreassinable licenses, cleans up methods used for counting.
2025-01-22 10:49:47 -08:00
Godfrey M
34e8360b10 moves counts to licenses, allows bulk check in of unreassignable licenses 2025-01-22 10:46:28 -08:00
Godfrey Martinez
ca259ee4c3 Merge pull request #26 from Godmartinz/checkin_non_reassignable_license_cleanup
moved methods to licenseSeat model, clean up code, removed unused namespaces etc
2025-01-21 11:47:42 -08:00
Godfrey M
2143952a1e removed unused models, castged unreassignable_seat, clean up in general 2025-01-21 11:43:47 -08:00
Godfrey M
6bab6e7151 remove unintended change 2025-01-16 12:27:01 -08:00
Godfrey M
d217c2e295 last rename 2025-01-16 12:24:32 -08:00
Godfrey M
52bf0faaa5 renamed unassignable to reassignable_seat 2025-01-16 12:19:59 -08:00
Godfrey M
3f3f2bfc61 fixed factory and test 2025-01-16 12:14:03 -08:00
Godfrey M
f050864fb4 renamed dead to unassigned, replaced 0 and 1 with true and flase 2025-01-16 12:12:14 -08:00
Godfrey M
db11fc35f4 fixed typo, fixed variable name 2025-01-16 12:00:49 -08:00
Godfrey M
f47a2b10c0 updated column name, updated Api license checkin and out 2025-01-16 11:53:15 -08:00
Godfrey M
344b4e7d60 fixes test to check if checked in licenses is unavailable 2025-01-16 09:46:03 -08:00
Godfrey M
7a23372489 adds migration for column unavailable, changes logic to utlize value 2025-01-15 15:36:12 -08:00
Godfrey M
9da15a8e58 get button color correct 2025-01-13 14:44:26 -08:00
Godfrey M
50e0e4a07b remove unrelated change 2025-01-13 14:37:03 -08:00
Godfrey M
5e1562ae4c adds count on tabs and reports and index 2025-01-13 14:31:54 -08:00
Godfrey M
8a0ed49623 allows checkin of unreassignable license seats 2025-01-13 13:35:48 -08:00
1375 changed files with 41842 additions and 8222 deletions

View File

@@ -3206,7 +3206,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4",
"profile": "https://github.com/swift2512",
"contributions": [
"bug"
"bug",
"code"
]
},
{
@@ -4198,6 +4199,42 @@
"contributions": [
"code"
]
},
{
"login": "FlorestanII",
"name": "Johannes Pollitt",
"avatar_url": "https://avatars.githubusercontent.com/u/15015119?v=4",
"profile": "https://github.com/FlorestanII",
"contributions": [
"code"
]
},
{
"login": "strobelm",
"name": "Michael Strobel",
"avatar_url": "https://avatars.githubusercontent.com/u/14185442?v=4",
"profile": "https://strobelm.de",
"contributions": [
"code"
]
},
{
"login": "nickwest",
"name": "Nicky West",
"avatar_url": "https://avatars.githubusercontent.com/u/634790?v=4",
"profile": "http://nickwest.me",
"contributions": [
"code"
]
},
{
"login": "akaspeh1",
"name": "akaspeh1",
"avatar_url": "https://avatars.githubusercontent.com/u/1347327?v=4",
"profile": "https://github.com/akaspeh1",
"contributions": [
"code"
]
}
]
}

View File

@@ -193,11 +193,17 @@ LDAP_TIME_LIM=600
IMPORT_TIME_LIMIT=600
IMPORT_MEMORY_LIMIT=500M
REPORT_TIME_LIMIT=12000
REQUIRE_SAML=false
API_THROTTLE_PER_MINUTE=120
CSV_ESCAPE_FORMULAS=true
LIVEWIRE_URL_PREFIX=null
# --------------------------------------------
# OPTIONAL: SAML SETTINGS
# --------------------------------------------
REQUIRE_SAML=false
SAML_KEY_SIZE=2048
# --------------------------------------------
# OPTIONAL: HASHING
# --------------------------------------------

137
.github/ISSUE_TEMPLATE/Bug-Report.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
name: Bug Report
description: File a bug report.
title: "[Bug]: "
projects: ["grokability/snipe-it"]
type: bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Most issues are documented in the [Snipe-IT repository's issues](https://github.com/grokability/snipe-it/issues) or in the official [Common Issues section of the Documentation](https://snipe-it.readme.io/docs/common-issues#/) and are due to the following:
- `.env` misconfiguration
- [Server Permissions](https://snipe-it.readme.io/docs/debugging-permissions#/)
- [Database Migrations](https://snipe-it.readme.io/docs/database-issues#run-migrations)
Please make sure you've checked these resources before submitting a new issue. If you find an existing issue, please add your context to it instead of opening a new issue. If your issue is more of a question, consider [opening a new discussion](https://github.com/grokability/snipe-it/discussions) or [pop by our Discord](https://discord.gg/yZFtShAcKk) instead of creating an issue.
**Please write your bug report in English.** You can use tools like [DeepL](https://www.deepl.com) or [Google Translate](https://translate.google.com/) to translate if necessary.
**If you choose to upload screenshots or videos (which we always encourage), please make sure they do not contain any sensitive information.**
- type: input
id: version
attributes:
label: Snipe-IT Version
description: What version of Snipe-IT are you seeing this issue on? You can find the version number in the footer of any page in Snipe-IT.
placeholder: ex. v8.3.1 - build 19577 (master)
validations:
required: true
- type: input
id: db-version
attributes:
label: MySQL/MariaDB version
description: What database are you using, and what version?
placeholder: ex. MySQL 5.7
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: How did you install Snipe-IT?
options:
- Git install
- Manual install (downloading zip/tar.gz)
- Docker
- install.sh
- Hosted by Grokability
- Other
- Not sure
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see! (Be nice!)
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- Other
- type: dropdown
id: on-demo
attributes:
label: Can you reproduce this on the public demo?
description: You can check this at https://demo.snipeitapp.com.
options:
- 'Yes'
- 'No'
- N/A
validations:
required: true
- type: dropdown
id: fmcs
attributes:
label: Do you have full multiple company support enabled?
description: You can check this in your Snipe-IT installation at `Admin Settings > General Settings > Scoping`.
options:
- 'Yes'
- 'No'
validations:
required: true
- type: dropdown
id: fmcs-location
attributes:
label: If you have full multiple company support enabled, do you have location scoping to company enabled?
description: You can check this in your Snipe-IT installation at `Admin Settings > General Settings > Scoping`.
options:
- 'Yes'
- 'No'
- I do not have full multiple company support enabled
validations:
required: true
- type: textarea
id: server-logs
attributes:
label: Application log output
description: Please copy and paste any relevant log output from `storage/logs/laravel.log`. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: textarea
id: browser-logs
attributes:
label: Browser console output
description: Please copy and paste any relevant log output from your browser console. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: checkboxes
id: common-issues
attributes:
label: Common Issues
description: Please make sure you have done the following before submitting your issue.
options:
- label: I have searched this repo for existing issues related to my issue (including closed issues)
required: true
- label: My APP_URL is set correctly in my .env file (including http or https and no trailing slash)
required: true
- label: I have searched the official Snipe-IT documentation and have checked the Common Issues documentation (where applicable)
required: true
- label: I have run database migrations (where applicable).
required: true
- label: I have attached screenshots and/or videos of the issue (where applicable)
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/grokability/snipe-it/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@@ -0,0 +1,38 @@
name: Feature Request
description: Request a new feature.
title: "[Feature]: "
projects: ["grokability/snipe-it"]
type: feature
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! Please make sure to search the existing issues in this repository to see if your feature has already been requested, and feel free to add your context to any existing requests.
**Please write your issue in English.** You can use tools like [DeepL](https://www.deepl.com) or [Google Translate](https://translate.google.com/) to translate if necessary.
**If you choose to upload screenshots or videos (which we always encourage), please make sure they do not contain any sensitive information.**
- type: input
id: version
attributes:
label: Snipe-IT Version
description: What version of Snipe-IT are you currently running? You can find the version number in the footer of any page in Snipe-IT.
placeholder: ex. v8.3.1 - build 19577 (master)
validations:
required: true
- type: textarea
id: feature-description
attributes:
label: How can we help?
description: Let us know in detail what feature you'd like to see added. While we can't promise to implement every feature request, we do read every one and take them into consideration when planning future releases.
placeholder: Tell us what you'd like to see in Snipe-IT! (Be nice!)
validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/grokability/snipe-it/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@@ -26,7 +26,7 @@ jobs:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -32,7 +32,7 @@ jobs:
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Crowdin push
uses: crowdin/github-action@v2

View File

@@ -42,7 +42,7 @@ jobs:
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
uses: actions/checkout@v5
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx

View File

@@ -42,7 +42,7 @@ jobs:
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
uses: actions/checkout@v5
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx

View File

@@ -11,7 +11,7 @@ jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Docker Hub Description
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4

View File

@@ -11,7 +11,7 @@ jobs:
issues: write
# pull-requests: write
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
debug-only: true
ascending: true

View File

@@ -37,7 +37,7 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get Composer Cache Directory
id: composer-cache
@@ -76,4 +76,16 @@ jobs:
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

View File

@@ -34,7 +34,7 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get Composer Cache Directory
id: composer-cache
@@ -75,4 +75,16 @@ jobs:
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

View File

@@ -25,7 +25,7 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get Composer Cache Directory
id: composer-cache
@@ -61,4 +61,16 @@ jobs:
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

View File

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

View File

@@ -55,6 +55,8 @@ class LdapSync extends Command
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
// Map the LDAP attributes to the Snipe-IT user fields.
$ldap_map = [
"username" => Setting::getSettings()->ldap_username_field,
"last_name" => Setting::getSettings()->ldap_lname_field,
@@ -63,11 +65,17 @@ class LdapSync extends Command
"emp_num" => Setting::getSettings()->ldap_emp_num,
"email" => Setting::getSettings()->ldap_email,
"phone" => Setting::getSettings()->ldap_phone_field,
"mobile" => Setting::getSettings()->ldap_mobile,
"jobtitle" => Setting::getSettings()->ldap_jobtitle,
"address" => Setting::getSettings()->ldap_address,
"city" => Setting::getSettings()->ldap_city,
"state" => Setting::getSettings()->ldap_state,
"zip" => Setting::getSettings()->ldap_zip,
"country" => Setting::getSettings()->ldap_country,
"location" => Setting::getSettings()->ldap_location,
"dept" => Setting::getSettings()->ldap_dept,
"manager" => Setting::getSettings()->ldap_manager,
"display_name" => Setting::getSettings()->ldap_display_name,
];
$ldap_default_group = Setting::getSettings()->ldap_default_group;
@@ -234,9 +242,11 @@ class LdapSync extends Command
}
// Assign the mapped LDAP attributes for each user to the Snipe-IT user fields
for ($i = 0; $i < $results['count']; $i++) {
$item = [];
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
$item['display_name'] = $results[$i][$ldap_map["display_name"]][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
@@ -244,8 +254,13 @@ class LdapSync extends Command
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
$item['mobile'] = $results[$i][$ldap_map["mobile"]][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? '';
$item['address'] = $results[$i][$ldap_map["address"]][0] ?? '';
$item['city'] = $results[$i][$ldap_map["city"]][0] ?? '';
$item['state'] = $results[$i][$ldap_map["state"]][0] ?? '';
$item['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
$item['zip'] = $results[$i][$ldap_map["zip"]][0] ?? '';
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
@@ -278,6 +293,9 @@ class LdapSync extends Command
if($ldap_map["username"] != null){
$user->username = $item['username'];
}
if($ldap_map["display_name"] != null){
$user->display_name = $item['display_name'];
}
if($ldap_map["last_name"] != null){
$user->last_name = $item['lastname'];
}
@@ -293,6 +311,9 @@ class LdapSync extends Command
if($ldap_map["phone"] != null){
$user->phone = $item['telephone'];
}
if($ldap_map["mobile"] != null){
$user->mobile = $item['mobile'];
}
if($ldap_map["jobtitle"] != null){
$user->jobtitle = $item['jobtitle'];
}

View File

@@ -6,6 +6,7 @@ use Illuminate\Console\Command;
use App\Models\Setting;
use Exception;
use Illuminate\Support\Facades\Crypt;
use App\Models\Ldap;
/**
* Check if a given ip is in a network
@@ -160,7 +161,15 @@ class LdapTroubleshooter extends Command
$output[] = "-x";
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
$output[] = "-w ".escapeshellarg(Crypt::Decrypt($settings->ldap_pword));
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
$output[] = "-w ". escapeshellarg($w);
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
if($settings->ldap_tls) {
$this->line("# adding STARTTLS option");
@@ -171,6 +180,23 @@ class LdapTroubleshooter extends Command
$this->line(implode(" \\\n",$output));
exit(0);
}
//PHP Version check for warning
$php_version = phpversion();
list($major, $minor, $patch) = explode('.', $php_version);
if (
$major < 8 ||
($major == 8 && $minor < 3) ||
($major == 8 && $minor == 3 && $patch < 21) ||
($major == 8 && $minor == 4 && $patch < 7)
) {
$this->warn("PHP Version: $php_version WARNING - Versions before 8.3.21 or 8.4.7 will return INCONSISTENT results!");
if (!$this->confirm("Are you sure you wish to continue?")) {
$this->warn("ABORTING");
exit(-1);
}
}
if(!$this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?');
if(!$confirmation) {
@@ -179,7 +205,7 @@ class LdapTroubleshooter extends Command
}
}
//$this->line(print_r($settings,true));
$this->info("STAGE 1: Checking settings");
$this->line("STAGE 1: Checking settings");
if(!$settings->ldap_enabled) {
$this->error("WARNING: Snipe-IT's LDAP setting is not turned on. (That may be OK if you're still trying to figure out settings)");
}
@@ -210,32 +236,40 @@ class LdapTroubleshooter extends Command
$this->info("Determined LDAP hostname to be: ".$parsed['host']);
}
$this->info("Performing DNS lookup of: ".$parsed['host']);
$ips = dns_get_record($parsed['host']);
$raw_ips = [];
//$this->info("Host IP is: ".print_r($ips,true));
if (inet_pton($parsed['host']) !== false) {
$this->line($parsed['host'] . " already looks like an address; skipping DNS lookup");
$raw_ips[] = $parsed['host'];
} else {
$this->line("Performing DNS lookup of: " . $parsed['host']);
$ips = dns_get_record($parsed['host']);
if(!$ips || count($ips) == 0) {
$this->error("ERROR: DNS lookup of host: ".$parsed['host']." has failed. ABORTING.");
exit(-1);
}
$this->debugout("IP's? ".print_r($ips,true));
foreach($ips as $ip) {
if(!isset($ip['ip'])) {
continue;
//$this->info("Host IP is: ".print_r($ips,true));
if (!$ips || count($ips) == 0) {
$this->error("ERROR: DNS lookup of host: " . $parsed['host'] . " has failed. ABORTING.");
exit(-1);
}
$raw_ips[]=$ip['ip'];
if($ip['ip'] == "127.0.0.1") {
$this->debugout("IP's? " . print_r($ips, true));
foreach ($ips as $ip) {
if (!isset($ip['ip'])) {
continue;
}
$raw_ips[] = $ip['ip'];
}
}
foreach ($raw_ips as $ip) {
if ($ip == "127.0.0.1") {
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
}
if(ip_in_range($ip['ip'],'10.0.0.0/8') || ip_in_range($ip['ip'],'192.168.0.0/16') || ip_in_range($ip['ip'], '172.16.0.0/12')) {
if (ip_in_range($ip, '10.0.0.0/8') || ip_in_range($ip, '192.168.0.0/16') || ip_in_range($ip, '172.16.0.0/12')) {
$this->error("WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network");
}
}
$this->info("STAGE 2: Checking basic network connectivity");
$ports = [389,636];
$this->line("STAGE 2: Checking basic network connectivity");
$ports = [636, 389];
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
$ports[] = $parsed['port'];
}
@@ -246,7 +280,7 @@ class LdapTroubleshooter extends Command
$errstr = '';
$timeout = 30.0;
$result = '';
$this->info("Attempting to connect to port: ".$port." - may take up to $timeout seconds");
$this->line("Attempting to connect to port: " . $port . " - may take up to $timeout seconds");
try {
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
} catch(Exception $e) {
@@ -265,9 +299,9 @@ class LdapTroubleshooter extends Command
exit(-1);
}
$this->info("STAGE 3: Determine encryption algorithm, if any");
$this->line("STAGE 3: Determine encryption algorithm, if any");
$ldap_urls = [];
$ldap_urls = []; // [url, cert-check?, start_tls?]
$pretty_ldap_urls = [];
foreach($open_ports as $port) {
$this->line("Trying TLS first for port $port");
@@ -275,35 +309,46 @@ class LdapTroubleshooter extends Command
if($this->test_anonymous_bind($ldap_url)) {
$this->info("Anonymous bind succesful to $ldap_url!");
$ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
$pretty_ldap_urls[] = [$ldap_url, "enabled", "n/a (no)"];
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
} else {
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
}
if($this->test_anonymous_bind($ldap_url, false)) {
$this->info("Anonymous bind succesful to $ldap_url with certifcate-checks disabled");
$ldap_urls[] = [ $ldap_url, false, false ];
$pretty_ldap_urls[] = [ $ldap_url, "no", "no" ];
$this->info("Anonymous bind successful to $ldap_url with certificate-checks disabled");
$ldap_urls[] = [$ldap_url, false, false];
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "n/a (no)"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS");
}
// now switching to ldap:// URL's from ldaps://
$ldap_url = "ldap://".$parsed['host'].":$port";
if($this->test_anonymous_bind($ldap_url, true, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS succesful!");
$ldap_urls[] = [ $ldap_url, true, true ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "YES" ];
$pretty_ldap_urls[] = [$ldap_url, "enabled", "STARTTLS ENABLED"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without STARTTLS");
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without certificate checks.");
}
if ($this->test_anonymous_bind($ldap_url, false, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS and cert checks *disabled* successful!");
$ldap_urls[] = [$ldap_url, false, true];
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "STARTTLS ENABLED"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled, and cert checks disabled. Trying without STARTTLS");
}
if($this->test_anonymous_bind($ldap_url)) {
$this->info("Plain connection to $ldap_url succesful!");
$ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
$pretty_ldap_urls[] = [$ldap_url, "n/a", "starttls disabled"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
@@ -313,23 +358,29 @@ class LdapTroubleshooter extends Command
$this->debugout(print_r($ldap_urls,true));
if(count($ldap_urls) > 0 ) {
$this->info("Found working LDAP URL's: ");
$this->debugout("Found working LDAP URL's: ");
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
$this->info("LDAP URL: ".$ldap_url[0]);
$this->info($ldap_url[0]. ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled"). ($ldap_url[2] ? " STARTTLS Enabled ": " STARTTLS Disabled"));
$this->debugout("LDAP URL: " . $ldap_url[0]);
$this->debugout($ldap_url[0] . ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled") . ($ldap_url[2] ? " STARTTLS Enabled " : " STARTTLS Disabled"));
}
$this->table(["URL", "Cert Checks Enabled?", "STARTTLS Enabled?"],$pretty_ldap_urls);
$this->table(["URL", "Cert Checks?", "STARTTLS?"], $pretty_ldap_urls);
} else {
$this->error("ERROR - no valid LDAP URL's available - ABORTING");
exit(1);
}
$this->info("STAGE 4: Test Administrative Bind for LDAP Sync");
$this->line("STAGE 4: Test Administrative Bind for LDAP Sync");
foreach($ldap_urls AS $ldap_url) {
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, Crypt::decrypt($settings->ldap_pword));
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, $w);
}
$this->info("STAGE 5: Test BaseDN");
$this->line("STAGE 5: Test BaseDN");
//grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
$all_defined_constants = get_defined_constants();
$ldap_constants = [];
@@ -341,16 +392,23 @@ class LdapTroubleshooter extends Command
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
foreach($ldap_urls AS $ldap_url) {
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword),$settings)) {
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,$w,$settings)) {
$this->info("Success getting informational bind!");
} else {
$this->error("Unable to get information from bind.");
}
}
$this->info("STAGE 6: Test LDAP Login to Snipe-IT");
$this->line("STAGE 6: Test LDAP Login to Snipe-IT");
foreach($ldap_urls AS $ldap_url) {
$this->info("Starting auth to ".$ldap_url[0]);
$this->line("Starting auth to " . $ldap_url[0]);
while(true) {
$with_tls = $ldap_url[1] ? "with": "without";
$with_startssl = $ldap_url[2] ? "using": "not using";
@@ -359,7 +417,12 @@ class LdapTroubleshooter extends Command
}
$username = $this->ask("Username");
$password = $this->secret("Password");
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
$results = $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
if ($results) {
$this->info("Success authenticating with " . $username);
} else {
$this->error("Unable to authenticate with " . $username);
}
}
}
@@ -368,14 +431,17 @@ class LdapTroubleshooter extends Command
public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
{
if ($check_cert) {
$this->line("we *ARE* checking certs");
Ldap::ignoreCertificates(false);
} else {
$this->line("we are IGNORING certs");
Ldap::ignoreCertificates(true);
}
$lconn = ldap_connect($ldap_url);
ldap_set_option($lconn, LDAP_OPT_PROTOCOL_VERSION, 3); // should we 'test' different protocol versions here? Does anyone even use anything other than LDAPv3?
// no - it's formally deprecated: https://tools.ietf.org/html/rfc3494
if(!$check_cert) {
putenv('LDAPTLS_REQCERT=never'); // This is horrible; is this *really* the only way to do it?
} else {
putenv('LDAPTLS_REQCERT'); // have to very explicitly and manually *UN* set the env var here to ensure it works
}
if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
// client-side TLS certificate support for LDAP (Google Secure LDAP)
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
@@ -404,9 +470,10 @@ class LdapTroubleshooter extends Command
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$this->info("gonna try to bind now, this can take a while if we mess it up");
$this->line("Attempting to bind now, this can take a while if we mess it up");
$bind_results = ldap_bind($lconn);
$this->info("Bind results are: ".$bind_results." which translate into boolean: ".(bool)$bind_results);
$this->line("Bind results are: " . $bind_results . " which translate into boolean: " . (bool)$bind_results);
ldap_close($lconn);
return (bool)$bind_results;
} catch (Exception $e) {
$this->error("WARNING: Exception caught during bind - ".$e->getMessage());
@@ -421,6 +488,7 @@ class LdapTroubleshooter extends Command
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($lconn, $username, $password);
ldap_close($lconn);
if(!$bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username");
return false;
@@ -446,22 +514,62 @@ class LdapTroubleshooter extends Command
return false;
}
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
$results = ldap_get_entries($conn, $result);
$cleaned_results = $this->ldap_results_cleaner($results);
$this->line(print_r($cleaned_results,true));
//okay, great - now how do we display those results? I have no idea.
$cleaned_results = [];
try {
// This _may_ only work for Active Directory?
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
$results = ldap_get_entries($conn, $result);
$cleaned_results = $this->ldap_results_cleaner($results);
//$this->line(print_r($cleaned_results,true));
$default_naming_contexts = $cleaned_results[0]['namingcontexts'];
$this->info("Default Naming Contexts:");
$this->info(implode(", ", $default_naming_contexts));
//okay, great - now how do we display those results? I have no idea.
} catch (\Exception $e) {
$this->error("Unable to get base naming contexts - here's what we *did* get:");
$this->line(print_r($cleaned_results, true));
}
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
$this->comment("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
$this->debugout("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
$this->debugout("Base DN is: ".$settings->ldap_basedn." and filter is: ".parenthesized_filter($settings->ldap_filter));
$search_results = ldap_search($conn, $settings->ldap_basedn, parenthesized_filter($settings->ldap_filter));
$entries = ldap_get_entries($conn, $search_results);
$this->info("Printing first 10 results: ");
for($i=0;$i<10;$i++) {
$this->info($search_results[$i]);
$pretty_data = array_slice($this->ldap_results_cleaner($entries), 0, 10);
//print_r($data);
$headers = [];
foreach ($pretty_data as $row) {
//populate headers
foreach ($row as $key => $value) {
//skip objectsid and objectguid because it junks up output
if ($key == "objectsid" || $key == "objectguid") {
continue;
}
if (!in_array($key, $headers)) {
$headers[] = $key;
}
}
}
$table = [];
//repeat again to populate table
foreach ($pretty_data as $row) {
$newrow = [];
foreach ($headers as $header) {
if (is_array(@$row[$header])) {
$newrow[] = "[" . implode(", ", $row[$header]) . "]";
} else {
$newrow[] = @$row[$header];
}
}
$table[] = $newrow;
}
$this->table($headers, $table);
} catch (\Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false;
} finally {
ldap_close($conn);
}
});
}
@@ -477,7 +585,7 @@ class LdapTroubleshooter extends Command
{
if(!(function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) {
// POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout)
$this->info('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
$this->line('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
return $function();
} else {
$parent_pid = posix_getpid();
@@ -514,4 +622,6 @@ class LdapTroubleshooter extends Command
}
}
}

View File

@@ -59,6 +59,9 @@ class PaveIt extends Command
'migrations',
'settings',
'users',
'telescope_entries',
'telescope_entries_tags',
'telescope_monitoring',
];
// We only need to find out what these are so we can nuke these columns on the assets table.

View File

@@ -65,7 +65,7 @@ class Purge extends Command
$maintenances = 0;
foreach ($assets as $asset) {
$this->info('- Asset "'.$asset->present()->name().'" deleted.');
$this->info('- Asset "'.$asset->display_name.'" deleted.');
$asset_assoc += $asset->assetlog()->count();
$asset->assetlog()->forceDelete();
$maintenances += $asset->maintenances()->count();

View File

@@ -77,7 +77,7 @@ class SendAcceptanceReminder extends Command
if(!$email){
$no_email_list[] = [
'id' => $acceptance->assignedTo?->id,
'name' => $acceptance->assignedTo?->present()->fullName(),
'name' => $acceptance->assignedTo?->display_name,
];
} else {
$count++;

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Helpers\Helper;
use App\Mail\ExpiringAssetsMail;
use App\Mail\ExpiringLicenseMail;
use App\Models\Asset;
@@ -52,19 +53,35 @@ class SendExpirationAlerts extends Command
->filter(fn($item) => !empty($item))
->all();
// Expiring Assets
$assets = Asset::getExpiringWarrantee($alert_interval);
$assets = Asset::getExpiringWarrantyOrEol($alert_interval);
if ($assets->count() > 0) {
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
$this->table(
['ID', 'Tag', 'Model', 'Model Number', 'EOL', 'EOL Months', 'Warranty Expires', 'Warranty Months'],
$assets->map(fn($item) => ['ID' => $item->id, 'Tag' => $item->asset_tag, 'Model' => $item->model->name, 'Model Number' => $item->model->model_number, 'EOL' => $item->asset_eol_date, 'EOL Months' => $item->model->eol, 'Warranty Expires' => $item->warranty_expires, 'Warranty Months' => $item->warranty_months])
);
}
// Expiring licenses
$licenses = License::getExpiringLicenses($alert_interval);
if ($licenses->count() > 0) {
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval));
$this->table(
['ID', 'Name', 'Expires', 'Termination Date'],
$licenses->map(fn($item) => ['ID' => $item->id, 'Name' => $item->name, 'Expires' => $item->expiration_date, 'Termination Date' => $item->termination_date])
);
}
// Send a message even if the count is 0
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
} else {
if ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings');

View File

@@ -95,7 +95,7 @@ class Helper
$Parsedown->setSafeMode(true);
if ($str) {
return $Parsedown->text($str);
return $Parsedown->text(strip_tags($str));
}
}
@@ -105,7 +105,7 @@ class Helper
$Parsedown->setSafeMode(true);
if ($str) {
return $Parsedown->line($str);
return $Parsedown->line(strip_tags($str));
}
}
@@ -435,6 +435,34 @@ class Helper
return $colors[$index];
}
/**
* Check if a string has any RTL characters
* @param $value
* @return bool
*/
public static function hasRtl($string) {
$rtlChar = '/[\x{0590}-\x{083F}]|[\x{08A0}-\x{08FF}]|[\x{FB1D}-\x{FDFF}]|[\x{FE70}-\x{FEFF}]/u';
return preg_match($rtlChar, $string) != 0;
}
// is chinese, japanese or korean language
public static function isCjk($string) {
return Helper::isChinese($string) || Helper::isJapanese($string) || Helper::isKorean($string);
}
public static function isChinese($string) {
return preg_match("/\p{Han}+/u", $string);
}
public static function isJapanese($string) {
return preg_match('/[\x{4E00}-\x{9FBF}\x{3040}-\x{309F}\x{30A0}-\x{30FF}]/u', $string);
}
public static function isKorean($string) {
return preg_match('/[\x{3130}-\x{318F}\x{AC00}-\x{D7AF}]/u', $string);
}
/**
* Increases or decreases the brightness of a color by a percentage of the current brightness.
*
@@ -1197,19 +1225,30 @@ class Helper
'webp' => 'far fa-image',
'avif' => 'far fa-image',
'svg' => 'fas fa-vector-square',
// word
'doc' => 'far fa-file-word',
'docx' => 'far fa-file-word',
// Excel
'xls' => 'far fa-file-excel',
'xlsx' => 'far fa-file-excel',
'ods' => 'far fa-file-excel',
// Presentation
'ppt' => 'far fa-file-powerpoint',
'odp' => 'far fa-file-powerpoint',
// archive
'zip' => 'fas fa-file-archive',
'rar' => 'fas fa-file-archive',
//Text
'odt' => 'far fa-file-alt',
'txt' => 'far fa-file-alt',
'rtf' => 'far fa-file-alt',
'xml' => 'fas fa-code',
// Misc
'pdf' => 'far fa-file-pdf',
'lic' => 'far fa-save',
@@ -1695,5 +1734,5 @@ class Helper
}
}
return $mismatched;
}
}
}

View File

@@ -16,6 +16,7 @@ class IconHelper
case 'clone':
return 'far fa-clone';
case 'delete':
case 'upload deleted':
return 'fas fa-trash';
case 'create':
return 'fa-solid fa-plus';

View File

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

View File

@@ -71,6 +71,7 @@ class AccessoryCheckoutController extends Controller
$this->authorize('checkout', $accessory);
$target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
$accessory->checkout_qty = $request->input('checkout_qty', 1);

View File

@@ -8,33 +8,23 @@ use App\Events\ItemAccepted;
use App\Events\ItemDeclined;
use App\Http\Controllers\Controller;
use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\Contracts\Acceptable;
use App\Models\Setting;
use App\Models\User;
use App\Models\AssetModel;
use App\Models\Accessory;
use App\Models\License;
use App\Models\Component;
use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use App\Helpers\Helper;
class AcceptanceController extends Controller
{
@@ -85,6 +75,11 @@ class AcceptanceController extends Controller
public function store(Request $request, $id) : RedirectResponse
{
$acceptance = CheckoutAcceptance::find($id);
$assigned_user = User::find($acceptance->assigned_to_id);
$settings = Setting::getSettings();
$path_logo = '';
$sig_filename='';
if (is_null($acceptance)) {
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
@@ -107,140 +102,75 @@ class AcceptanceController extends Controller
}
/**
* Get the signature and save it
* Check for the signature directory
*/
if (! Storage::exists('private_uploads/signatures')) {
Storage::makeDirectory('private_uploads/signatures', 775);
}
/**
* Check for the eula-pdfs directory
*/
if (! Storage::exists('private_uploads/eula-pdfs')) {
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
}
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
$display_model = '';
$pdf_view_route = '';
$pdf_filename = 'accepted-eula-'.date('Y-m-d-h-i-s').'.pdf';
$sig_filename='';
// If signatures are required, make sure we have one
if (Setting::getSettings()->require_accept_signature == '1') {
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
// Get the data array ready for the notifications and PDF generation
$data = [
'item_tag' => $item->asset_tag,
'item_name' => $item->name, // this handles licenses seats, which don't have a 'name' field
'item_model' => $item->model?->name,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'eula' => $item->getEula(),
'note' => $request->input('note'),
'check_out_date' => Helper::getFormattedDateObject($acceptance->created_at, 'datetime', false),
'accepted_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false),
'declined_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false),
'assigned_to' => $assigned_user->display_name,
'site_name' => $settings->site_name,
'company_name' => $item->company?->name?? $settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => ($settings->acceptance_pdf_logo) ? public_path() . '/uploads/' . $settings->acceptance_pdf_logo : null,
'date_settings' => $settings->date_display_format,
'admin' => auth()->user()->present()?->fullName,
'qty' => $acceptance->qty ?? 1,
];
if ($request->input('asset_acceptance') == 'accepted') {
/**
* Check for the eula-pdfs directory
*/
if (! Storage::exists('private_uploads/eula-pdfs')) {
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
}
if (Setting::getSettings()->require_accept_signature == '1') {
// Check if the signature directory exists, if not create it
if (!Storage::exists('private_uploads/signatures')) {
Storage::makeDirectory('private_uploads/signatures', 775);
}
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
$assigned_user = User::find($acceptance->assigned_to_id);
// this is horrible
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
$pdf_view_route ='account.accept.accept-asset-eula';
$asset_model = AssetModel::find($item->model_id);
if (!$asset_model) {
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
}
$display_model = $asset_model->name;
break;
case 'App\Models\Accessory':
$pdf_view_route ='account.accept.accept-accessory-eula';
$accessory = Accessory::find($item->id);
$display_model = $accessory->name;
break;
case 'App\Models\LicenseSeat':
$pdf_view_route ='account.accept.accept-license-eula';
$license = License::find($item->license_id);
$display_model = $license->name;
break;
case 'App\Models\Component':
$pdf_view_route ='account.accept.accept-component-eula';
$component = Component::find($item->id);
$display_model = $component->name;
break;
case 'App\Models\Consumable':
$pdf_view_route ='account.accept.accept-consumable-eula';
$consumable = Consumable::find($item->id);
$display_model = $consumable->name;
break;
}
// if ($acceptance->checkoutable_type == 'App\Models\Asset') {
// $pdf_view_route ='account.accept.accept-asset-eula';
// $asset_model = AssetModel::find($item->model_id);
// $display_model = $asset_model->name;
// $assigned_to = User::find($item->assigned_to)->present()->fullName;
//
// } elseif ($acceptance->checkoutable_type== 'App\Models\Accessory') {
// $pdf_view_route ='account.accept.accept-accessory-eula';
// $accessory = Accessory::find($item->id);
// $display_model = $accessory->name;
// $assigned_to = User::find($item->assignedTo);
//
// }
/**
* Gather the data for the PDF. We fire this whether there is a signature required or not,
* since we want the moment-in-time proof of what the EULA was when they accepted it.
*/
$branding_settings = SettingsController::getPDFBranding();
$path_logo = "";
// Check for the PDF logo path and use that, otherwise use the regular logo path
if (!is_null($branding_settings->acceptance_pdf_logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->acceptance_pdf_logo;
} elseif (!is_null($branding_settings->logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->logo;
}
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $display_model,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'eula' => $item->getEula(),
'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
'assigned_to' => $assigned_user->present()->fullName,
'company_name' => $branding_settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
'date_settings' => $branding_settings->date_display_format,
'admin' => auth()->user()->present()?->fullName,
];
if ($pdf_view_route!='') {
Log::debug($pdf_filename.' is the filename, and the route was specified.');
$pdf = Pdf::loadView($pdf_view_route, $data);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
}
// Generate the PDF content
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf_content);
// Log the acceptance
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
// Send the PDF to the signing user
@@ -248,15 +178,14 @@ class AcceptanceController extends Controller
// Add the attachment for the signing user into the $data array
$data['file'] = $pdf_filename;
try {
$assigned_user->notify(new AcceptanceAssetAcceptedToUserNotification($data));
$assigned_user->notify((new AcceptanceAssetAcceptedToUserNotification($data))->locale($assigned_user->locale));
} catch (\Exception $e) {
Log::warning($e);
}
}
try {
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
$acceptance->notify((new AcceptanceAssetAcceptedNotification($data))->locale(Setting::getSettings()->locale));
} catch (\Exception $e) {
Log::warning($e);
}
@@ -264,95 +193,21 @@ class AcceptanceController extends Controller
$return_msg = trans('admin/users/message.accepted');
// Item was declined
} else {
/**
* Check for the eula-pdfs directory
*/
if (! Storage::exists('private_uploads/eula-pdfs')) {
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) {
$acceptance->decline($sig_filename, $request->input('note'));
}
if (Setting::getSettings()->require_accept_signature == '1') {
// Check if the signature directory exists, if not create it
if (!Storage::exists('private_uploads/signatures')) {
Storage::makeDirectory('private_uploads/signatures', 775);
}
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
// Format the data to send the declined notification
$branding_settings = SettingsController::getPDFBranding();
// This is the most horriblest
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
$asset_model = AssetModel::find($item->model_id);
$display_model = $asset_model->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Accessory':
$accessory = Accessory::find($item->id);
$display_model = $accessory->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\LicenseSeat':
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Component':
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Consumable':
$consumable = Consumable::find($item->id);
$display_model = $consumable->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
}
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $display_model,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'note' => $request->input('note'),
'declined_date' => Carbon::parse($acceptance->declined_at)->format('Y-m-d'),
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'assigned_to' => $assigned_to,
'company_name' => $branding_settings->site_name,
'date_settings' => $branding_settings->date_display_format,
];
if ($pdf_view_route!='') {
Log::debug($pdf_filename.' is the filename, and the route was specified.');
$pdf = Pdf::loadView($pdf_view_route, $data);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
}
$acceptance->decline($sig_filename, $request->input('note'));
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
Log::debug('New event acceptance.');
event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined');
}
// Send an email notification if one is requested
if ($acceptance->alert_on_response_id) {
try {
$recipient = User::find($acceptance->alert_on_response_id);
@@ -371,9 +226,10 @@ class AcceptanceController extends Controller
Log::warning($e);
}
}
return redirect()->to('account/accept')->with('success', $return_msg);
}
}

View File

@@ -3,11 +3,13 @@
namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Models\Actionlog;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use \Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use \Illuminate\Http\Response;
class ActionlogController extends Controller
{
public function displaySig($filename) : RedirectResponse | Response | bool
@@ -39,17 +41,29 @@ class ActionlogController extends Controller
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
{
$this->authorize('view', \App\Models\Asset::class);
if (config('filesystems.default') == 's3_private') {
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
if ($actionlog = Actionlog::where('filename', $filename)->with('user')->with('target')->firstOrFail()) {
$this->authorize('view', $actionlog->target);
$this->authorize('view', $actionlog->user);
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)) {
if (request()->input('inline') == 'true') {
return response()->file(config('app.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'));
}
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'));
return redirect()->back()->with('error', trans('general.record_not_found'));
}
}

View File

@@ -288,32 +288,42 @@ class AccessoriesController extends Controller
'note' => $request->input('note'),
]);
$accessory_checkout->created_by = auth()->id();
$accessory_checkout->save();
$payload = [
'accessory_id' => $accessory->id,
'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'),
'created_by' => auth()->id(),
'pivot' => $accessory_checkout->id,
];
}
// Set this value to be able to pass the qty through to the event
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
}
/**
* Check in the item so that it can be checked out again to someone else
*
* @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request
* @param int $accessoryUserId
* @param string $backto
* @return \Illuminate\Http\RedirectResponse
* @return JsonResponse
* @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>]
* @internal param int $accessoryId
*/
public function checkin(Request $request, $accessoryUserId = null)
{
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist', ['id' => $accessoryUserId])));
}
$accessory = Accessory::find($accessory_checkout->accessory_id);
@@ -327,7 +337,14 @@ class AccessoriesController extends Controller
$user = User::find($accessory_checkout->assigned_to);
}
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkin.success')));
$payload = [
'accessory_id' => $accessory->id,
'note' => $request->input('note'),
'created_by' => auth()->id(),
'pivot' => $accessory_checkout->id,
];
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkin.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkin.error')));

View File

@@ -50,6 +50,7 @@ class AssetModelsController extends Controller
'fieldset',
'deleted_at',
'updated_at',
'require_serial',
];
$assetmodels = AssetModel::select([
@@ -69,6 +70,7 @@ class AssetModelsController extends Controller
'models.fieldset_id',
'models.deleted_at',
'models.updated_at',
'models.require_serial'
])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
->withCount('assets as assets_count');

View File

@@ -146,6 +146,7 @@ class AssetsController extends Controller
'model.category',
'model.manufacturer',
'model.fieldset',
'model.depreciation',
'supplier'
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
@@ -609,7 +610,7 @@ class AssetsController extends Controller
$asset->use_text = $asset->present()->fullName;
if (($asset->checkedOutToUser()) && ($asset->assigned)) {
$asset->use_text .= ' → ' . $asset->assigned->getFullNameAttribute();
$asset->use_text .= ' → ' . $asset->assigned->display_name;
}
@@ -1289,9 +1290,19 @@ class AssetsController extends Controller
public function assignedAssets(Request $request, Asset $asset) : JsonResponse | array
{
$this->authorize('view', Asset::class);
$this->authorize('view', $asset);
return [];
// to do
$query = Asset::where([
'assigned_to' => $asset->id,
'assigned_type' => Asset::class,
]);
$total = $query->count();
$assets = $query->applyOffsetAndLimit($total)->get();
return (new AssetsTransformer)->transformAssets($assets, $total);
}
public function assignedAccessories(Request $request, Asset $asset) : JsonResponse | array

View File

@@ -38,6 +38,8 @@ class CategoriesController extends Controller
'consumables_count',
'components_count',
'licenses_count',
'created_at',
'updated_at',
'image',
'notes',
];

View File

@@ -230,13 +230,13 @@ class ConsumablesController extends Controller
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
'user' => ($consumable_assignment->user) ? [
'id' => (int) $consumable_assignment->user->id,
'name'=> e($consumable_assignment->user->present()->fullName()),
'name'=> e($consumable_assignment->user->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
'created_by' => ($consumable_assignment->adminuser) ? [
'id' => (int) $consumable_assignment->adminuser->id,
'name'=> e($consumable_assignment->adminuser->present()->fullName()),
'name'=> e($consumable_assignment->adminuser->display_name),
] : null,
];
}

View File

@@ -69,7 +69,7 @@ class ImportController extends Controller
if (function_exists('iconv')) {
$file_contents = $file->getContent(); //TODO - this *does* load the whole file in RAM, but we need that to be able to 'iconv' it?
$encoding = $detector->getEncoding($file_contents);
\Log::warning("Discovered encoding: $encoding in uploaded CSV");
\Log::debug("Discovered encoding: $encoding in uploaded CSV");
$reader = null;
if (strcasecmp($encoding, 'UTF-8') != 0) {
$transliterated = false;
@@ -103,7 +103,7 @@ class ImportController extends Controller
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
try {
$import->header_row = $reader->fetchOne(0);
$import->header_row = $reader->nth(0);
} catch (JsonEncodingException $e) {
return response()->json(
Helper::formatStandardApiResponse(
@@ -136,7 +136,7 @@ class ImportController extends Controller
try {
// Grab the first row to display via ajax as the user picks fields
$import->first_row = $reader->fetchOne(1);
$import->first_row = $reader->nth(1);
} catch (JsonEncodingException $e) {
return response()->json(
Helper::formatStandardApiResponse(
@@ -195,7 +195,7 @@ class ImportController extends Controller
// Run a backup immediately before processing
if ($request->get('run-backup')) {
Log::debug('Backup manually requested via importer');
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H:i:s')]);
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H-i-s')]);
} else {
Log::debug('NO BACKUP requested via importer');
}

View File

@@ -128,7 +128,9 @@ class LicenseSeatsController extends Controller
// nothing to update
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
if( $touched && $licenseSeat->unreassignable_seat) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
}
// the logging functions expect only one "target". if both asset and user are present in the request,
// we simply let assets take precedence over users...
if ($licenseSeat->isDirty('assigned_to')) {
@@ -145,7 +147,11 @@ class LicenseSeatsController extends Controller
if ($licenseSeat->save()) {
if ($is_checkin) {
$licenseSeat->logCheckin($target, $request->input('notes'));
if(!$licenseSeat->license->reassignable){
$licenseSeat->unreassignable_seat = true;
$licenseSeat->save();
}
$licenseSeat->logCheckin($target, $licenseSeat->notes);
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}

View File

@@ -26,6 +26,12 @@ class LicensesController extends Controller
$licenses = License::with('company', 'manufacturer', 'supplier','category', 'adminuser')->withCount('freeSeats as free_seats_count');
if ($request->input('status')=='inactive') {
$licenses->ExpiredLicenses();
} else {
$licenses->ActiveLicenses();
}
if ($request->filled('company_id')) {
$licenses->where('licenses.company_id', '=', $request->input('company_id'));
}
@@ -94,6 +100,8 @@ class LicensesController extends Controller
$licenses->onlyTrashed();
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $licenses->count()) ? $licenses->count() : app('api_offset_value');
$limit = app('api_limit_value');

View File

@@ -37,10 +37,14 @@ class LocationsController extends Controller
'address',
'address2',
'assets_count',
'assets_count',
'assigned_assets_count',
'rtd_assets_count',
'accessories_count',
'assigned_accessories_count',
'assigned_assets_count',
'assigned_assets_count',
'components_count',
'consumables_count',
'users_count',
'children_count',
'city',
'country',
'created_at',
@@ -54,7 +58,6 @@ class LocationsController extends Controller
'rtd_assets_count',
'state',
'updated_at',
'users_count',
'zip',
'notes',
];
@@ -79,8 +82,9 @@ class LocationsController extends Controller
'locations.currency',
'locations.company_id',
'locations.notes',
'locations.created_by',
'locations.deleted_at',
])
->withCount('assignedAssets as assigned_assets_count')
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
@@ -88,6 +92,8 @@ class LocationsController extends Controller
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->with('adminuser');
// Only scope locations if the setting is enabled
@@ -131,6 +137,14 @@ class LocationsController extends Controller
$locations->where('locations.company_id', '=', $request->input('company_id'));
}
if ($request->filled('parent_id')) {
$locations->where('locations.parent_id', '=', $request->input('parent_id'));
}
if ($request->input('status') == 'deleted') {
$locations->onlyTrashed();
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
$limit = app('api_limit_value');
@@ -224,8 +238,13 @@ class LocationsController extends Controller
])
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->findOrFail($id);
return (new LocationsTransformer)->transformLocation($location);
@@ -320,11 +339,15 @@ class LocationsController extends Controller
{
$this->authorize('delete', Location::class);
$location = Location::withCount('assignedAssets as assigned_assets_count')
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withCount('accessories as accessories_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->findOrFail($id);
if (! $location->isDeletable()) {

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
/**
* This class controls all API actions related to notes for
* the Snipe-IT Asset Management application.
*/
class NotesController extends Controller
{
/**
* Retrieve a list of manual notes (action logs) for a given asset.
*
* Checks authorization to view assets, attempts to find the asset by ID,
* and fetches related action log entries of type 'note added', including
* user information for each note. Returns a JSON response with the notes or errors.
*
* @param \Illuminate\Http\Request $request The incoming HTTP request.
* @param Asset $asset The ID of the asset whose notes to retrieve.
* @return \Illuminate\Http\JsonResponse
*/
public function index(Asset $asset): JsonResponse
{
$this->authorize('view', $asset);
// Get the manual notes for the asset
$notes = ActionLog::with('user:id,username')
->where('item_type', Asset::class)
->where('item_id', $asset->id)
->where('action_type', 'note added')
->orderBy('created_at', 'desc')
->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'item_type', 'action_type', 'target_id', 'target_type']);
$notesArray = $notes->map(function ($note) {
return [
'id' => $note->id,
'created_at' => $note->created_at,
'note' => $note->note,
'created_by' => $note->created_by,
'username' => $note->user?->username, // adding the username
'item_id' => $note->item_id,
'item_type' => $note->item_type,
'action_type' => $note->action_type,
];
});
// Return a success response
return response()->json(Helper::formatStandardApiResponse('success', ['notes' => $notesArray, 'asset_id' => $asset->id]));
}
/**
* Store a manual note on a specified asset and log the action.
*
* Checks authorization for updating assets, validates the presence of the 'note',
* attempts to find the asset by ID, and creates a new ActionLog entry if successful.
* Returns JSON responses indicating success or failure with appropriate HTTP status codes.
*
* @param \Illuminate\Http\Request $request The incoming HTTP request containing the 'note'.
* @param Asset $asset The ID of the asset to attach the note to.
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request, Asset $asset): JsonResponse
{
$this->authorize('update', $asset);
if ($request->input('note', '') == '') {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.required', ['attribute' => 'note'])), 422);
}
// Create the note
$logaction = new ActionLog();
$logaction->item_type = get_class($asset);
$logaction->created_by = Auth::id();
$logaction->item_id = $asset->id;
$logaction->note = $request->input('note', '');
if ($logaction->logaction('note added')) {
// Return a success response
return response()->json(Helper::formatStandardApiResponse('success', ['note' => $logaction->note, 'item_id' => $asset->id], trans('general.note_added')));
}
// Return an error response if something went wrong
return response()->json(Helper::formatStandardApiResponse('error', null, 'Something went wrong'), 500);
}
}

View File

@@ -69,7 +69,7 @@ class ProfileController extends Controller
if ($checkoutRequest && $checkoutRequest->itemRequested()) {
$assets = [
'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()),
'name' => e($checkoutRequest->itemRequested()->present()->name()),
'name' => e($checkoutRequest->itemRequested()->display_name),
'type' => e($checkoutRequest->itemType()),
'qty' => (int) $checkoutRequest->quantity,
'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null,

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Http\Transformers\DatatablesTransformer;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
@@ -51,10 +50,22 @@ class SettingsController extends Controller
})->slice(0, 10)->map(function ($item) use ($settings) {
return (object) [
'username' => $item[$settings['ldap_username_field']][0] ?? null,
'display_name' => $item[$settings['ldap_display_name']][0] ?? null,
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
'email' => $item[$settings['ldap_email']][0] ?? null,
'phone' => $item[$settings['ldap_phone_field']][0] ?? null,
'mobile' => $item[$settings['ldap_mobile']][0] ?? null,
'jobtitle' => $item[$settings['ldap_jobtitle']][0] ?? null,
'department' => $item[$settings['ldap_department']][0] ?? null,
'manager' => $item[$settings['ldap_manager']][0] ?? null,
'address' => $item[$settings['ldap_address']][0] ?? null,
'city' => $item[$settings['ldap_city']][0] ?? null,
'state' => $item[$settings['ldap_state']][0] ?? null,
'zip' => $item[$settings['ldap_zip']][0] ?? null,
'country' => $item[$settings['ldap_country']][0] ?? null,
'location' => $item[$settings['ldap_location']][0] ?? null,
];
});
if ($users->count() > 0) {
@@ -78,7 +89,7 @@ class SettingsController extends Controller
}
} catch (\Exception $e) {
Log::debug('Connection failed but we cannot debug it any further on our end.');
return response()->json(['message' => $e->getMessage()], 500);
return response()->json(['message' => $e->getMessage()], 400);
}

View File

@@ -32,7 +32,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object);
if (!$object) {
@@ -51,11 +51,7 @@ class UploadedFilesController extends Controller
];
$uploads = Actionlog::select('action_logs.*')
->whereNotNull('filename')
->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)
->where('action_type', '=', 'uploaded')
$uploads = self::$map_object_type[$object_type]::withTrashed()->find($id)->uploads()
->with('adminuser');
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
@@ -96,7 +92,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object);
if (!$object) {
@@ -144,7 +140,7 @@ class UploadedFilesController extends Controller
public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object);
if (!$object) {
@@ -188,7 +184,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
@@ -206,7 +202,7 @@ class UploadedFilesController extends Controller
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
if ($log->logUploadDelete($object, $log->filename)) {
return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
}

View File

@@ -20,6 +20,7 @@ use App\Models\Consumable;
use App\Models\License;
use App\Models\User;
use App\Notifications\CurrentInventory;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;
@@ -64,6 +65,7 @@ class UsersController extends Controller
'users.jobtitle',
'users.last_login',
'users.last_name',
'users.display_name',
'users.locale',
'users.location_id',
'users.manager_id',
@@ -154,6 +156,10 @@ class UsersController extends Controller
$users = $users->where('users.last_name', '=', $request->input('last_name'));
}
if ($request->filled('display_name')) {
$users = $users->where('users.display_name', '=', $request->input('display_name'));
}
if ($request->filled('employee_num')) {
$users = $users->where('users.employee_num', '=', $request->input('employee_num'));
}
@@ -284,6 +290,7 @@ class UsersController extends Controller
[
'last_name',
'first_name',
'display_name',
'email',
'jobtitle',
'username',
@@ -355,6 +362,7 @@ class UsersController extends Controller
'users.employee_num',
'users.first_name',
'users.last_name',
'users.display_name',
'users.gravatar',
'users.avatar',
'users.email',
@@ -365,20 +373,17 @@ class UsersController extends Controller
$users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->get('search'))
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
->orWhere('display_name', 'LIKE', '%'.$request->get('search').'%')
->orWhere('email', 'LIKE', '%'.$request->get('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
});
}
$users = $users->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
$users = $users->orderBy('display_name', 'asc')->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
$users = $users->paginate(50);
foreach ($users as $user) {
$name_str = '';
if ($user->last_name != '') {
$name_str .= $user->last_name.', ';
}
$name_str .= $user->first_name;
$name_str = $user->display_name;
if ($user->username != '') {
$name_str .= ' ('.$user->username.')';
@@ -430,9 +435,20 @@ class UsersController extends Controller
$user->password = $user->noPassword();
}
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) {
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
try {
$user->notify(new WelcomeNotification($user));
} catch (\Exception $e) {
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
}
}
if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups'));
} else {
@@ -511,6 +527,10 @@ class UsersController extends Controller
$user->username = $request->input('username');
}
if ($request->filled('display_name')) {
$user->display_name = $request->input('display_name');
}
if ($request->filled('email')) {
$user->email = $request->input('email');
}
@@ -540,7 +560,7 @@ class UsersController extends Controller
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
}
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) {
// Check if the request has groups passed and has a value, AND that the user us a superuser

View File

@@ -82,6 +82,7 @@ class AssetModelsController extends Controller
$model->notes = $request->input('notes');
$model->created_by = auth()->id();
$model->requestable = $request->has('requestable');
$model->require_serial = $request->input('require_serial', 0);
if ($request->input('fieldset_id') != '') {
$model->fieldset_id = $request->input('fieldset_id');
@@ -155,7 +156,7 @@ class AssetModelsController extends Controller
$model->category_id = $request->input('category_id');
$model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0');
$model->require_serial = $request->input('require_serial', 0);
$model->fieldset_id = $request->input('fieldset_id');
if ($model->save()) {

View File

@@ -65,6 +65,8 @@ class AssetCheckoutController extends Controller
*/
public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse
{
try {
// Check if the asset exists
if (! $asset = Asset::find($assetId)) {
@@ -81,6 +83,7 @@ class AssetCheckoutController extends Controller
$admin = auth()->user();
$target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
$asset = $this->updateAssetLocation($asset, $target);

View File

@@ -110,17 +110,35 @@ class AssetsController extends Controller
// This is only necessary on create, not update, since bulk editing is handled
// differently
$asset_tags = $request->input('asset_tags');
$model = AssetModel::find($request->input('model_id'));
$serial_errors = [];
$serials = $request->input('serials');
$settings = Setting::getSettings();
//Validate required serial based on model setting
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
if ($model && $model->require_serial === 1 && empty($serials[$a])) {
$serial_errors["serials.$a"] = trans('admin/hardware/form.serial_required', ['number' => $a]);
}
}
if (!empty($serial_errors)) {
return redirect()->back()
->withInput()
->withErrors($serial_errors);
}
$asset = null;
$companyId = Company::getIdForCurrentUser($request->input('company_id'));
$successes = [];
$failures = [];
$serials = $request->input('serials');
$asset = null;
for ($a = 1; $a <= count($asset_tags); $a++) {
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
$asset = new Asset();
$asset->model()->associate(AssetModel::find($request->input('model_id')));
$asset->model()->associate($model);
$asset->name = $request->input('name');
// Check for a corresponding serial
@@ -132,7 +150,7 @@ class AssetsController extends Controller
$asset->asset_tag = $asset_tags[$a];
}
$asset->company_id = Company::getIdForCurrentUser($request->input('company_id'));
$asset->company_id = $companyId;
$asset->model_id = $request->input('model_id');
$asset->order_number = $request->input('order_number');
$asset->notes = $request->input('notes');
@@ -172,7 +190,6 @@ class AssetsController extends Controller
// Update custom fields in the database.
// Validation for these fields is handled through the AssetRequest form request
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
@@ -453,6 +470,13 @@ class AssetsController extends Controller
]);
//Validate required serial based on model setting
if ($model && $model->require_serial === 1 && empty($serial[1])) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
'asset_model' => $model->name
]));
}
if ($asset->save()) {
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.update.success'));
@@ -797,7 +821,7 @@ class AssetsController extends Controller
'item_id' => $asset->id,
'item_type' => Asset::class,
'created_by' => auth()->id(),
'note' => 'Checkout imported by '.auth()->user()->present()->fullName().' from history importer',
'note' => 'Checkout imported by '.auth()->user()->display_name.' from history importer',
'target_id' => $item[$asset_tag][$batch_counter]['user_id'],
'target_type' => User::class,
'created_at' => $item[$asset_tag][$batch_counter]['checkout_date'],
@@ -825,7 +849,7 @@ class AssetsController extends Controller
'item_id' => $item[$asset_tag][$batch_counter]['asset_id'],
'item_type' => Asset::class,
'created_by' => auth()->id(),
'note' => 'Checkin imported by '.auth()->user()->present()->fullName().' from history importer',
'note' => 'Checkin imported by '.auth()->user()->display_name.' from history importer',
'target_id' => null,
'created_at' => $checkin_date,
'action_type' => 'checkin',

View File

@@ -637,6 +637,7 @@ class BulkAssetsController extends Controller
$admin = auth()->user();
$target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
if (! is_array($request->get('selected_assets'))) {
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
@@ -646,6 +647,15 @@ class BulkAssetsController extends Controller
$assets = Asset::findOrFail($asset_ids);
// Prevent checking out assets that are already checked out
if ($assets->pluck('assigned_to')->unique()->filter()->isNotEmpty()) {
// re-add the asset ids so the assets select is re-populated
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect(route('hardware.bulkcheckout.show'))
->with('error', trans('general.error_assets_already_checked_out'));
}
if (request('checkout_to_type') == 'asset') {
foreach ($asset_ids as $asset_id) {
if ($target->id == $asset_id) {

View File

@@ -92,7 +92,9 @@ class BulkAssetModelsController extends Controller
$update_array['min_amt'] = $request->input('min_amt');
}
if ($request->filled('require_serial')) {
$update_array['require_serial'] = $request->input('require_serial');
}
if (count($update_array) > 0) {
AssetModel::whereIn('id', $models_raw_array)->update($update_array);

View File

@@ -102,13 +102,15 @@ class ComponentCheckoutController extends Controller
return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_user_company'));
}
$component->checkout_qty = $request->input('assigned_qty');
// Update the component data
$component->asset_id = $request->input('asset_id');
$component->assets()->attach($component->id, [
'component_id' => $component->id,
'created_by' => auth()->user()->id,
'created_at' => date('Y-m-d H:i:s'),
'assigned_qty' => $request->input('assigned_qty'),
'assigned_qty' => $component->checkout_qty,
'asset_id' => $request->input('asset_id'),
'note' => $request->input('note'),
]);

View File

@@ -64,12 +64,7 @@ class LicenseCheckinController extends Controller
$this->authorize('checkout', $license);
if (! $license->reassignable) {
// Not allowed to checkin
Session::flash('error', trans('admin/licenses/message.checkin.not_reassignable') . '.');
return redirect()->back()->withInput();
}
// Declare the rules for the form validation
$rules = [
@@ -98,6 +93,9 @@ class LicenseCheckinController extends Controller
$licenseSeat->assigned_to = null;
$licenseSeat->asset_id = null;
$licenseSeat->notes = $request->input('notes');
if (! $licenseSeat->license->reassignable) {
$licenseSeat->unreassignable_seat = true;
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($request->get('redirect_option') === 'target'){
@@ -106,7 +104,7 @@ class LicenseCheckinController extends Controller
// Was the asset updated?
if ($licenseSeat->save()) {
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes')));
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $licenseSeat->notes));
return Helper::getRedirectOption($request, $license->id, 'Licenses')
@@ -132,21 +130,17 @@ class LicenseCheckinController extends Controller
$license = License::findOrFail($licenseId);
$this->authorize('checkin', $license);
if (! $license->reassignable) {
// Not allowed to checkin
Session::flash('error', 'License not reassignable.');
return redirect()->back()->withInput();
}
$licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId)
->whereNotNull('assigned_to')
->with('user')
->with('user', 'license')
->get();
$license = $licenseSeatsByUser->first()?->license;
foreach ($licenseSeatsByUser as $user_seat) {
$user_seat->assigned_to = null;
if ($license && ! $license->reassignable) {
$user_seat->unreassignable_seat = true;
}
if ($user_seat->save()) {
Log::debug('Checking in '.$license->name.' from user '.$user_seat->username);
$user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
@@ -159,9 +153,12 @@ class LicenseCheckinController extends Controller
->get();
$count = 0;
$license = $licenseSeatsByAsset->first()?->license;
foreach ($licenseSeatsByAsset as $asset_seat) {
$asset_seat->asset_id = null;
if ($license && ! $license->reassignable) {
$asset_seat->unreassignable_seat = true;
}
if ($asset_seat->save()) {
Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag);
$asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg'));

View File

@@ -25,7 +25,7 @@ class LicenseCheckoutController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param $id
* @return \Illuminate\Contracts\View\View
* @return \Illuminate\Contracts\View\View |\Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(License $license)
@@ -39,6 +39,16 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
// Make sure the license is expired or terminated
if ($license->isInactive()) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
}
// We don't currently allow checking out licenses to locations, so we'll reset that to user if needed
if (session()->get('checkout_to_type') == 'location') {
session()->put(['checkout_to_type' => 'user']);
}
// Return the checkout view
return view('licenses/checkout', compact('license'));
}
@@ -65,22 +75,31 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found'));
}
$this->authorize('checkout', $license);
// Make sure there is at least one available to checkout
if ($license->availCount()->count() < 1) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
// Make sure the license is expired or terminated
if ($license->isInactive()) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
}
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
$licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes');
$checkoutMethod = 'checkoutTo'.ucwords(request('checkout_to_type'));
if ($request->filled('asset_id')) {
session()->put(['checkout_to_type' => 'asset']);
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
$request->request->add(['assigned_asset' => $checkoutTarget->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'asset']);
} elseif ($request->filled('assigned_to')) {
session()->put(['checkout_to_type' => 'user']);
$checkoutTarget = $this->checkoutToUser($licenseSeat);
$request->request->add(['assigned_user' => $checkoutTarget->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'user']);
@@ -89,6 +108,7 @@ class LicenseCheckoutController extends Controller
if ($checkoutTarget) {
return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.checkout.success'));
}
@@ -110,6 +130,7 @@ class LicenseCheckoutController extends Controller
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats')));
}
if (! $licenseSeat->license->is($license)) {
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.mismatch')));
}

View File

@@ -245,16 +245,28 @@ class LicensesController extends Controller
$license = License::with('assignedusers')->find($license->id);
$users_count = User::where('autoassign_licenses', '1')->count();
$total_seats_count = $license->totalSeatsByLicenseID();
$total_seats_count = (int) $license->totalSeatsByLicenseID();
$available_seats_count = $license->availCount()->count();
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
$unreassignable_seats_count = License::unReassignableCount($license);
if(!$license->reassignable){
$checkedout_seats_count = ($total_seats_count - $available_seats_count - $unreassignable_seats_count );
}
else {
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
}
if($license->isInactive()){
session()->flash('warning', (trans('admin/licenses/message.checkout.license_is_inactive')));
}
$this->authorize('view', $license);
return view('licenses.view', compact('license'))
->with('users_count', $users_count)
->with('total_seats_count', $total_seats_count)
->with('available_seats_count', $available_seats_count)
->with('checkedout_seats_count', $checkedout_seats_count);
->with('checkedout_seats_count', $checkedout_seats_count)
->with('unreassignable_seats_count', $unreassignable_seats_count);
}
@@ -364,7 +376,7 @@ class LicensesController extends Controller
$license->order_number,
$license->free_seat_count,
$license->seats,
($license->adminuser ? $license->adminuser->present()->fullName() : trans('admin/reports/general.deleted_user')),
($license->adminuser ? $license->adminuser->display_name : trans('admin/reports/general.deleted_user')),
$license->depreciation ? $license->depreciation->name: '',
$license->updated_at,
$license->deleted_at,

View File

@@ -189,30 +189,36 @@ class LocationsController extends Controller
{
$this->authorize('delete', Location::class);
if (is_null($location = Location::find($locationId))) {
$location = Location::withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->find($locationId);
if (!$location) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.does_not_exist'));
}
if ($location->users()->count() > 0) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_users'));
} elseif ($location->children()->count() > 0) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_child_loc'));
} elseif ($location->assets()->count() > 0) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_assets'));
} elseif ($location->assignedassets()->count() > 0) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_assets'));
}
if ($location->isDeletable()) {
if ($location->image) {
try {
Storage::disk('public')->delete('locations/'.$location->image);
} catch (\Exception $e) {
Log::error($e);
if ($location->image) {
try {
Storage::disk('public')->delete('locations/'.$location->image);
} catch (\Exception $e) {
Log::error($e);
}
}
$location->delete();
return redirect()->to(route('locations.index'))->with('success', trans('admin/locations/message.delete.success'));
} else {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_users'));
}
$location->delete();
return redirect()->to(route('locations.index'))->with('success', trans('admin/locations/message.delete.success'));
}
/**
@@ -247,23 +253,41 @@ class LocationsController extends Controller
$this->authorize('view', Location::class);
if ($location = Location::where('id', $id)->first()) {
$parent = Location::where('id', $location->parent_id)->first();
$manager = User::where('id', $location->manager_id)->first();
$company = Company::where('id', $location->company_id)->first();
$users = User::where('location_id', $id)->with('company', 'department', 'location')->get();
$assets = Asset::where('assigned_to', $id)->where('assigned_type', Location::class)->with('model', 'model.category')->get();
return view('locations/print')
->with('assets', $assets)
->with('users',$users)
->with('assigned', false)
->with('assets', $location->assets)
->with('assignedAssets', $location->assignedAssets)
->with('accessories', $location->accessories)
->with('assignedAccessories', $location->assignedAccessories)
->with('users',$location->users)
->with('location', $location)
->with('parent', $parent)
->with('manager', $manager)
->with('company', $company);
->with('consumables', $location->consumables)
->with('components', $location->components)
->with('children', $location->children);
}
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
}
public function print_all_assigned($id) : View | RedirectResponse
{
$this->authorize('view', Location::class);
if ($location = Location::where('id', $id)->first()) {
return view('locations/print')
->with('assigned', true)
->with('assets', $location->assets)
->with('assignedAssets', $location->assignedAssets)
->with('accessories', $location->accessories)
->with('assignedAccessories', $location->assignedAccessories)
->with('users',$location->users)
->with('location', $location)
->with('consumables', $location->consumables)
->with('components', $location->components)
->with('children', $location->children);
}
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
}
/**
* Returns a view that presents a form to clone a location.
@@ -321,33 +345,12 @@ class LocationsController extends Controller
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.restore.success'));
}
// Check validation
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.location'), 'error' => $location->getErrors()->first()]));
}
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
}
public function print_all_assigned($id) : View | RedirectResponse
{
$this->authorize('view', Location::class);
if ($location = Location::where('id', $id)->first()) {
$parent = Location::where('id', $location->parent_id)->first();
$manager = User::where('id', $location->manager_id)->first();
$company = Company::where('id', $location->company_id)->first();
$users = User::where('location_id', $id)->with('company', 'department', 'location')->get();
$assets = Asset::where('location_id', $id)->with('model', 'model.category')->get();
return view('locations/print')
->with('assets', $assets)
->with('users',$users)
->with('location', $location)
->with('parent', $parent)
->with('manager', $manager)
->with('company', $company);
}
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
}
/**
* Returns a view that allows the user to bulk delete locations
@@ -366,8 +369,12 @@ class LocationsController extends Controller
$locations = Location::whereIn('id', $locations_raw_array)
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->withCount('users as users_count')->get();
$valid_count = 0;
@@ -400,9 +407,13 @@ class LocationsController extends Controller
$locations = Location::whereIn('id', $locations_raw_array)
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')->get();
->withCount('users as users_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')->get();
$success_count = 0;
$error_count = 0;

View File

@@ -159,7 +159,7 @@ class ReportsController extends Controller
$row[] = e($asset->serial);
if ($target = $asset->assignedTo) {
$row[] = e($target->present()->name());
$row[] = e($target->display_name);
} else {
$row[] = ''; // Empty string if unassigned
}
@@ -274,22 +274,18 @@ class ReportsController extends Controller
$target_name = '';
if ($actionlog->target) {
if ($actionlog->targetType() == 'user') {
$target_name = $actionlog->target->getFullNameAttribute();
} else {
$target_name = $actionlog->target->getDisplayNameAttribute();
}
$target_name = $actionlog->target->display_name;
}
if($actionlog->item){
$item_name = e($actionlog->item->getDisplayNameAttribute());
if ($actionlog->item){
$item_name = e($actionlog->item->display_name);
} else {
$item_name = '';
}
$row = [
$actionlog->created_at,
($actionlog->adminuser) ? e($actionlog->adminuser->getFullNameAttribute()) : '',
($actionlog->adminuser) ? $actionlog->adminuser->display_name : '',
$actionlog->present()->actionType(),
e($actionlog->itemType()),
($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name,
@@ -298,10 +294,10 @@ class ReportsController extends Controller
(($actionlog->item) && ($actionlog->item->model)) ? $actionlog->item->model->model_number : null,
$target_name,
($actionlog->note) ? e($actionlog->note) : '',
$actionlog->log_meta,
$actionlog->remote_ip,
$actionlog->user_agent,
$actionlog->action_source,
$actionlog->log_meta,
];
fputcsv($handle, $row);
}
@@ -830,7 +826,7 @@ class ReportsController extends Controller
}
if ($request->filled('location')) {
$row[] = ($asset->location) ? $asset->location->present()->name() : '';
$row[] = ($asset->location) ? $asset->location->display_name : '';
}
if ($request->filled('location_address')) {
@@ -843,7 +839,7 @@ class ReportsController extends Controller
}
if ($request->filled('rtd_location')) {
$row[] = ($asset->defaultLoc) ? $asset->defaultLoc->present()->name() : '';
$row[] = ($asset->defaultLoc) ? $asset->defaultLoc->display_name : '';
}
if ($request->filled('rtd_location_address')) {
@@ -856,8 +852,8 @@ class ReportsController extends Controller
}
if ($request->filled('assigned_to')) {
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? $asset->assigned->getFullNameAttribute() : ($asset->assigned ? $asset->assigned->display_name : '');
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? 'user' : $asset->assignedType();
$row[] = ($asset->assigned) ? $asset->assigned->display_name : '';
$row[] = ($asset->assigned) ? $asset->assignedType() : '';
}
if ($request->filled('username')) {
@@ -1260,7 +1256,7 @@ class ReportsController extends Controller
$row[] = str_replace(',', '', e($item['assetItem']->model->name));
$row[] = str_replace(',', '', e($item['assetItem']->name));
$row[] = str_replace(',', '', e($item['assetItem']->asset_tag));
$row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->present()->name() : trans('admin/reports/general.deleted_user')));
$row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->display_name : trans('admin/reports/general.deleted_user')));
$rows[] = implode(',', $row);
}
}

View File

@@ -873,6 +873,7 @@ class SettingsController extends Controller
$setting->ldap_default_group = $request->input('ldap_default_group');
$setting->ldap_filter = $request->input('ldap_filter');
$setting->ldap_username_field = $request->input('ldap_username_field');
$setting->ldap_display_name = $request->input('ldap_display_name');
$setting->ldap_lname_field = $request->input('ldap_lname_field');
$setting->ldap_fname_field = $request->input('ldap_fname_field');
$setting->ldap_auth_filter_query = $request->input('ldap_auth_filter_query');
@@ -889,7 +890,12 @@ class SettingsController extends Controller
$setting->ldap_pw_sync = $request->input('ldap_pw_sync', '0');
$setting->custom_forgot_pass_url = $request->input('custom_forgot_pass_url');
$setting->ldap_phone_field = $request->input('ldap_phone');
$setting->ldap_mobile = $request->input('ldap_mobile');
$setting->ldap_jobtitle = $request->input('ldap_jobtitle');
$setting->ldap_address = $request->input('ldap_address');
$setting->ldap_city = $request->input('ldap_city');
$setting->ldap_state = $request->input('ldap_state');
$setting->ldap_zip = $request->input('ldap_zip');
$setting->ldap_country = $request->input('ldap_country');
$setting->ldap_location = $request->input('ldap_location');
$setting->ldap_dept = $request->input('ldap_dept');

View File

@@ -36,7 +36,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', $object);
if (!$object) {
@@ -85,7 +85,7 @@ class UploadedFilesController extends Controller
public function show($object_type, $id, $file_id) : RedirectResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object);
if (!$object) {
@@ -130,7 +130,7 @@ class UploadedFilesController extends Controller
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
@@ -139,7 +139,7 @@ class UploadedFilesController extends Controller
// Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
$log = Actionlog::where('id',$file_id)->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)->first();
if ($log) {
@@ -148,7 +148,7 @@ class UploadedFilesController extends Controller
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
if ($log->logUploadDelete($object, $log->filename)) {
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.success', 1));
}

View File

@@ -13,7 +13,9 @@ use App\Models\Company;
use App\Models\Group;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Notifications\CurrentInventory;
@@ -88,6 +90,7 @@ class UsersController extends Controller
//Username, email, and password need to be handled specially because the need to respect config values on an edit.
$user->email = trim($request->input('email'));
$user->username = trim($request->input('username'));
$user->display_name = $request->input('display_name');
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
@@ -127,7 +130,7 @@ class UsersController extends Controller
// we have to invoke the form request here to handle image uploads
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if($request->get('redirect_option') === 'back'){
if ($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
@@ -135,6 +138,18 @@ class UsersController extends Controller
if ($user->save()) {
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
try {
$user->notify(new WelcomeNotification($user));
} catch (\Exception $e) {
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
}
}
if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups'));
} else {
@@ -240,6 +255,7 @@ class UsersController extends Controller
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->display_name = $request->input('display_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
@@ -562,10 +578,10 @@ class UsersController extends Controller
$user->employee_num,
$user->first_name,
$user->last_name,
$user->present()->fullName(),
$user->display_name,
$user->username,
$user->email,
($user->manager) ? $user->manager->present()->fullName() : '',
($user->manager) ? $user->manager->display_name : '',
($user->userloc) ? $user->userloc->name : '',
($user->department) ? $user->department->name : '',
$user->assets->count(),

View File

@@ -185,7 +185,7 @@ class ViewAssetsController extends Controller
$logaction->target_type = User::class;
$data['item_quantity'] = $request->has('request-quantity') ? e($request->input('request-quantity')) : 1;
$data['requested_by'] = $user->present()->fullName();
$data['requested_by'] = $user->display_name;
$data['item'] = $item;
$data['item_type'] = $itemType;
$data['target'] = auth()->user();

View File

@@ -34,10 +34,7 @@ class AssetCountForSidebar
}
try {
$total_assets = Asset::count();
if ($settings->show_archived_in_list != '1') {
$total_assets -= Asset::Archived()->count();
}
$total_assets = Asset::AssetsForShow()->count();
view()->share('total_assets', $total_assets);
} catch (\Exception $e) {
Log::debug($e);

View File

@@ -109,7 +109,7 @@ class SettingsSamlRequest extends FormRequest
];
$pkey = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_bits' => config('app.saml_key_size'),
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);

View File

@@ -36,6 +36,7 @@ class AccessoriesTransformer
'qty' => ($accessory->qty) ? (int) $accessory->qty : null,
'purchase_date' => ($accessory->purchase_date) ? Helper::getFormattedDateObject($accessory->purchase_date, 'date') : null,
'purchase_cost' => Helper::formatCurrencyOutput($accessory->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($accessory->totalCostSum()),
'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null,
'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null, // Legacy - should phase out - replaced by below, for the bootstrap table formatter
'min_amt' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,
@@ -44,7 +45,7 @@ class AccessoriesTransformer
'checkouts_count' => $accessory->checkouts_count,
'created_by' => ($accessory->adminuser) ? [
'id' => (int) $accessory->adminuser->id,
'name'=> e($accessory->adminuser->present()->fullName()),
'name'=> e($accessory->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($accessory->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($accessory->updated_at, 'datetime'),

View File

@@ -50,17 +50,20 @@ class ActionlogsTransformer
public function transformActionlog (Actionlog $actionlog, $settings = null)
{
$icon = $actionlog->present()->icon();
if (($actionlog->filename!='') && ($actionlog->action_type!='upload deleted')) {
$icon = Helper::filetype_icon($actionlog->filename);
}
static $custom_fields = false;
if ($custom_fields === false) {
$custom_fields = CustomField::all();
}
if ($actionlog->filename!='') {
$icon = Helper::filetype_icon($actionlog->filename);
}
// This is necessary since we can't escape special characters within a JSON object
if (($actionlog->log_meta) && ($actionlog->log_meta!='')) {
@@ -144,15 +147,15 @@ class ActionlogsTransformer
[
'url' => $actionlog->uploads_file_url(),
'filename' => $actionlog->filename,
'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_url()),
'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_path()),
'exists_on_disk' => Storage::exists($actionlog->uploads_file_path()) ? true : false,
] : null,
'item' => ($actionlog->item) ? [
'id' => (int) $actionlog->item->id,
'name' => ($actionlog->itemType()=='user') ? e($actionlog->item->getFullNameAttribute()) : e($actionlog->item->getDisplayNameAttribute()),
'name' => e($actionlog->item->display_name) ?? null,
'type' => e($actionlog->itemType()),
'serial' =>e($actionlog->item->serial) ? e($actionlog->item->serial) : null
'serial' => e($actionlog->item->serial) ? e($actionlog->item->serial) : null
] : null,
'location' => ($actionlog->location) ? [
'id' => (int) $actionlog->location->id,
@@ -165,19 +168,19 @@ class ActionlogsTransformer
'action_type' => $actionlog->present()->actionType(),
'admin' => ($actionlog->adminuser) ? [
'id' => (int) $actionlog->adminuser->id,
'name' => e($actionlog->adminuser->getFullNameAttribute()),
'name' => e($actionlog->adminuser->display_name) ?? null,
'first_name'=> e($actionlog->adminuser->first_name),
'last_name'=> e($actionlog->adminuser->last_name)
] : null,
'created_by' => ($actionlog->adminuser) ? [
'id' => (int) $actionlog->adminuser->id,
'name' => e($actionlog->adminuser->getFullNameAttribute()),
'name' => e($actionlog->adminuser->display_name),
'first_name'=> e($actionlog->adminuser->first_name),
'last_name'=> e($actionlog->adminuser->last_name)
] : null,
'target' => ($actionlog->target) ? [
'id' => (int) $actionlog->target->id,
'name' => ($actionlog->targetType()=='user') ? e($actionlog->target->getFullNameAttribute()) : e($actionlog->target->getDisplayNameAttribute()),
'name' => e($actionlog->target->display_name) ?? null,
'type' => e($actionlog->targetType()),
] : null,

View File

@@ -65,10 +65,11 @@ class AssetModelsTransformer
'default_fieldset_values' => $default_field_values,
'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None',
'requestable' => ($assetmodel->requestable == '1') ? true : false,
'require_serial' => $assetmodel->require_serial,
'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes),
'created_by' => ($assetmodel->adminuser) ? [
'id' => (int) $assetmodel->adminuser->id,
'name'=> e($assetmodel->adminuser->present()->fullName()),
'name'=> e($assetmodel->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($assetmodel->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmodel->updated_at, 'datetime'),

View File

@@ -58,6 +58,13 @@ class AssetsTransformer
'id' => (int) $asset->model->manufacturer->id,
'name'=> e($asset->model->manufacturer->name),
] : null,
'depreciation' => (($asset->model) && ($asset->model->depreciation)) ? [
'id' => (int) $asset->model->depreciation->id,
'name'=> e($asset->model->depreciation->name),
'months'=> (int) $asset->model->depreciation->months,
'type'=> e($asset->model->depreciation->depreciation_type),
'minimum'=> ($asset->model->depreciation->depreciation_min) ? (int) $asset->model->depreciation->depreciation_min : null,
] : null,
'supplier' => ($asset->supplier) ? [
'id' => (int) $asset->supplier->id,
'name'=> e($asset->supplier->name),
@@ -84,7 +91,7 @@ class AssetsTransformer
'warranty_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null,
'created_by' => ($asset->adminuser) ? [
'id' => (int) $asset->adminuser->id,
'name'=> e($asset->adminuser->present()->fullName()),
'name'=> e($asset->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($asset->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($asset->updated_at, 'datetime'),
@@ -280,7 +287,7 @@ class AssetsTransformer
'id' => (int) $asset->id,
'image' => ($asset->getImageUrl()) ? $asset->getImageUrl() : null,
'type' => 'asset',
'name' => e($asset->present()->fullName()),
'name' => e($asset->display_name),
'model' => ($asset->model) ? e($asset->model->name) : null,
'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null,
'asset_tag' => e($asset->asset_tag),

View File

@@ -64,7 +64,7 @@ class CategoriesTransformer
'licenses_count' => (int) $category->licenses_count,
'created_by' => ($category->adminuser) ? [
'id' => (int) $category->adminuser->id,
'name'=> e($category->adminuser->present()->fullName()),
'name'=> e($category->adminuser->display_name),
] : null,
'notes' => Helper::parseEscapedMarkedownInline($category->notes),
'created_at' => Helper::getFormattedDateObject($category->created_at, 'datetime'),

View File

@@ -38,7 +38,7 @@ class CompaniesTransformer
'users_count' => (int) $company->users_count,
'created_by' => ($company->adminuser) ? [
'id' => (int) $company->adminuser->id,
'name'=> e($company->adminuser->present()->fullName()),
'name'=> e($company->adminuser->display_name),
] : null,
'notes' => Helper::parseEscapedMarkedownInline($company->notes),
'created_at' => Helper::getFormattedDateObject($company->created_at, 'datetime'),

View File

@@ -43,6 +43,7 @@ class ComponentsTransformer
'order_number' => e($component->order_number),
'purchase_date' => Helper::getFormattedDateObject($component->purchase_date, 'date'),
'purchase_cost' => Helper::formatCurrencyOutput($component->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($component->totalCostSum()),
'remaining' => (int) $component->numRemaining(),
'company' => ($component->company) ? [
'id' => (int) $component->company->id,
@@ -51,7 +52,7 @@ class ComponentsTransformer
'notes' => ($component->notes) ? Helper::parseEscapedMarkedownInline($component->notes) : null,
'created_by' => ($component->adminuser) ? [
'id' => (int) $component->adminuser->id,
'name'=> e($component->adminuser->present()->fullName()),
'name'=> e($component->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($component->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($component->updated_at, 'datetime'),
@@ -76,7 +77,7 @@ class ComponentsTransformer
$array[] = [
'assigned_pivot_id' => $asset->pivot->id,
'id' => (int) $asset->id,
'name' => e($asset->model->present()->name).' '.e($asset->present()->name),
'name' => e($asset->model->display_name).' '.e($asset->display_name),
'qty' => $asset->pivot->assigned_qty,
'note' => $asset->pivot->note,
'type' => 'asset',

View File

@@ -37,12 +37,13 @@ class ConsumablesTransformer
'remaining' => $consumable->numRemaining(),
'order_number' => e($consumable->order_number),
'purchase_cost' => Helper::formatCurrencyOutput($consumable->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($consumable->totalCostSum()),
'purchase_date' => Helper::getFormattedDateObject($consumable->purchase_date, 'date'),
'qty' => (int) $consumable->qty,
'notes' => ($consumable->notes) ? Helper::parseEscapedMarkedownInline($consumable->notes) : null,
'created_by' => ($consumable->adminuser) ? [
'id' => (int) $consumable->adminuser->id,
'name'=> e($consumable->adminuser->present()->fullName()),
'name'=> e($consumable->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($consumable->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($consumable->updated_at, 'datetime'),

View File

@@ -35,7 +35,7 @@ class DepartmentsTransformer
] : null,
'manager' => ($department->manager) ? [
'id' => (int) $department->manager->id,
'name' => e($department->manager->getFullNameAttribute()),
'name' => e($department->manager->display_name),
'first_name'=> e($department->manager->first_name),
'last_name'=> e($department->manager->last_name),
] : null,

View File

@@ -33,7 +33,7 @@ class DepreciationsTransformer
'licenses_count' => ($depreciation->licenses_count > 0) ? (int) $depreciation->licenses_count : 0,
'created_by' => ($depreciation->adminuser) ? [
'id' => (int) $depreciation->adminuser->id,
'name'=> e($depreciation->adminuser->present()->fullName()),
'name'=> e($depreciation->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($depreciation->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($depreciation->updated_at, 'datetime')

View File

@@ -29,7 +29,7 @@ class GroupsTransformer
'notes' => Helper::parseEscapedMarkedownInline($group->notes),
'created_by' => ($group->adminuser) ? [
'id' => (int) $group->adminuser->id,
'name'=> e($group->adminuser->present()->fullName()),
'name'=> e($group->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($group->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($group->updated_at, 'datetime'),

View File

@@ -7,7 +7,6 @@ use App\Models\License;
use App\Models\LicenseSeat;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
class LicenseSeatsTransformer
{
public function transformLicenseSeats(Collection $seats, $total)
@@ -52,6 +51,7 @@ class LicenseSeatsTransformer
'reassignable' => (bool) $seat->license->reassignable,
'notes' => e($seat->notes),
'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')),
'disabled' => $seat->unreassignable_seat || $seat->license->isInactive(),
];
$permissions_array['available_actions'] = [

View File

@@ -31,13 +31,13 @@ class LicensesTransformer
'purchase_order' => ($license->purchase_order) ? e($license->purchase_order) : null,
'purchase_date' => Helper::getFormattedDateObject($license->purchase_date, 'date'),
'termination_date' => Helper::getFormattedDateObject($license->termination_date, 'date'),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'depreciation' => ($license->depreciation) ? ['id' => (int) $license->depreciation->id,'name'=> e($license->depreciation->name)] : null,
'purchase_cost' => Helper::formatCurrencyOutput($license->purchase_cost),
'purchase_cost_numeric' => $license->purchase_cost,
'notes' => Helper::parseEscapedMarkedownInline($license->notes),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'seats' => (int) $license->seats,
'free_seats_count' => (int) $license->free_seats_count,
'free_seats_count' => (int) $license->free_seats_count - License::unReassignableCount($license),
'remaining' => (int) $license->free_seats_count,
'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null,
'license_name' => ($license->license_name) ? e($license->license_name) : null,
@@ -48,13 +48,13 @@ class LicensesTransformer
'category' => ($license->category) ? ['id' => (int) $license->category->id, 'name'=> e($license->category->name)] : null,
'created_by' => ($license->adminuser) ? [
'id' => (int) $license->adminuser->id,
'name'=> e($license->adminuser->present()->fullName()),
'name'=> e($license->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($license->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($license->deleted_at, 'datetime'),
'user_can_checkout' => (bool) ($license->free_seats_count > 0),
'disabled' => $license->isInactive(),
];
$permissions_array['available_actions'] = [

View File

@@ -53,6 +53,9 @@ class LocationsTransformer
'assets_count' => (int) $location->assets_count,
'rtd_assets_count' => (int) $location->rtd_assets_count,
'users_count' => (int) $location->users_count,
'consumables_count' => (int) $location->consumables_count,
'components_count' => (int) $location->components_count,
'children_count' => (int) $location->children_count,
'currency' => ($location->currency) ? e($location->currency) : null,
'ldap_ou' => ($location->ldap_ou) ? e($location->ldap_ou) : null,
'notes' => Helper::parseEscapedMarkedownInline($location->notes),
@@ -76,12 +79,13 @@ class LocationsTransformer
];
$permissions_array['available_actions'] = [
'update' => Gate::allows('update', Location::class) ? true : false,
'update' => (Gate::allows('update', Location::class) && ($location->deleted_at == '')),
'delete' => $location->isDeletable(),
'bulk_selectable' => [
'delete' => $location->isDeletable()
],
'clone' => (Gate::allows('create', Location::class) && ($location->deleted_at == '')),
'restore' => (Gate::allows('create', Location::class) && ($location->deleted_at != '')),
];
$array += $permissions_array;

View File

@@ -73,11 +73,11 @@ class MaintenancesTransformer
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
'user_id' => ($assetmaintenance->adminuser) ? [
'id' => $assetmaintenance->adminuser->id,
'name'=> e($assetmaintenance->adminuser->present()->fullName())
'name'=> e($assetmaintenance->adminuser->display_name)
] : null, // legacy to not change the shape of the API
'created_by' => ($assetmaintenance->adminuser) ? [
'id' => (int) $assetmaintenance->adminuser->id,
'name'=> e($assetmaintenance->adminuser->present()->fullName()),
'name'=> e($assetmaintenance->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),

View File

@@ -40,7 +40,7 @@ class ManufacturersTransformer
'notes' => Helper::parseEscapedMarkedownInline($manufacturer->notes),
'created_by' => ($manufacturer->adminuser) ? [
'id' => (int) $manufacturer->adminuser->id,
'name'=> e($manufacturer->adminuser->present()->fullName()),
'name'=> e($manufacturer->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($manufacturer->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($manufacturer->updated_at, 'datetime'),

View File

@@ -34,7 +34,7 @@ class PredefinedKitsTransformer
'name' => e($kit->name),
'created_by' => ($kit->adminuser) ? [
'id' => (int) $kit->adminuser->id,
'name'=> e($kit->adminuser->present()->fullName()),
'name'=> e($kit->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($kit->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($kit->updated_at, 'datetime'),

View File

@@ -4,7 +4,6 @@ namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Database\Eloquent\Collection;
class ProfileTransformer
@@ -26,7 +25,7 @@ class ProfileTransformer
'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename),
'item' => ($file->item) ? [
'name' => ($file->itemType()=='user') ? e($file->item->getFullNameAttribute()) : e($file->item->getDisplayNameAttribute()),
'name' => $file->item->display_name ? e($file->item->display_name) : null,
'type' => e($file->itemType()),
] : null,
'filename' => e($file->filename),

View File

@@ -32,7 +32,7 @@ class StatuslabelsTransformer
'notes' => e($statuslabel->notes),
'created_by' => ($statuslabel->adminuser) ? [
'id' => (int) $statuslabel->adminuser->id,
'name'=> e($statuslabel->adminuser->present()->fullName()),
'name'=> e($statuslabel->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($statuslabel->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($statuslabel->updated_at, 'datetime'),

View File

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

View File

@@ -22,7 +22,7 @@ class UsersTransformer
public function transformUser(User $user)
{
$role = '';
$role = null;
if ($user->isSuperUser()) {
$role = 'superadmin';
} elseif ($user->isAdmin()) {
@@ -31,16 +31,17 @@ class UsersTransformer
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar) ?? null,
'name' => e($user->getFullNameAttribute()),
'first_name' => e($user->first_name),
'last_name' => e($user->last_name),
'username' => e($user->username),
'name' => e($user->getFullNameAttribute()) ?? null,
'first_name' => e($user->first_name) ?? null,
'last_name' => e($user->last_name) ?? null,
'display_name' => ($user->getRawOriginal('display_name')) ? e($user->getRawOriginal('display_name')) : null,
'username' => e($user->username) ?? null,
'remote' => ($user->remote == '1') ? true : false,
'locale' => ($user->locale) ? e($user->locale) : null,
'employee_num' => ($user->employee_num) ? e($user->employee_num) : null,
'manager' => ($user->manager) ? [
'id' => (int) $user->manager->id,
'name'=> e($user->manager->first_name).' '.e($user->manager->last_name),
'name'=> e($user->manager->display_name),
] : null,
'jobtitle' => ($user->jobtitle) ? e($user->jobtitle) : null,
'vip' => ($user->vip == '1') ? true : false,
@@ -59,7 +60,7 @@ class UsersTransformer
] : null,
'department_manager' => ($user->department?->manager) ? [
'id' => (int) $user->department->manager->id,
'name'=> e($user->department->manager->full_name),
'name'=> e($user->department->manager->display_name),
] : null,
'location' => ($user->userloc) ? [
'id' => (int) $user->userloc->id,
@@ -82,7 +83,7 @@ class UsersTransformer
'company' => ($user->company) ? ['id' => (int) $user->company->id, 'name'=> e($user->company->name)] : null,
'created_by' => ($user->createdBy) ? [
'id' => (int) $user->createdBy->id,
'name'=> e($user->createdBy->present()->fullName),
'name'=> e($user->createdBy->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($user->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($user->updated_at, 'datetime'),
@@ -138,6 +139,7 @@ class UsersTransformer
'first_name' => e($user->first_name),
'last_name' => e($user->last_name),
'username' => e($user->username),
'display_name' => e($user->display_name),
'created_by' => $user->adminuser ? [
'id' => (int) $user->adminuser->id,
'name'=> e($user->adminuser->present()->fullName),

View File

@@ -40,11 +40,32 @@ class AssetModelImporter extends ItemImporter
{
$editingAssetModel = false;
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
/**
* This part gets a little confusing, since folks might be importing multiple models with the same name and different model numbers for the first time
* or they might be wanting to update existing models with new model numbers.
*/
// They are not trying to update existing models, so we'll check for duplicates with model name *and* number
if (! $this->updating) {
$this->log('Finding model by name and model number: '.$this->findCsvMatch($row, 'name').' / '.$this->findCsvMatch($row, 'model_number'));
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->where('model_number', '=', $this->findCsvMatch($row, 'model_number'))->first();
} else {
if ($this->findCsvMatch($row, 'id')!='') {
// Override model if an ID was given
$this->log('Finding model by ID: '.$this->findCsvMatch($row, 'id'));
$assetModel = AssetModel::find($this->findCsvMatch($row, 'id'));
} else {
$this->log('Finding model by name: '.$this->findCsvMatch($row, 'name'));
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
}
}
if ($assetModel) {
if (! $this->updating) {
$this->log('A matching Model '.$this->item['name'].' already exists');
$this->log('A matching Model '.$this->item['name'].' already exists and we are not updating. Skipping.');
return;
}
@@ -66,6 +87,7 @@ class AssetModelImporter extends ItemImporter
$this->item['fieldset'] = trim($this->findCsvMatch($row, 'fieldset'));
$this->item['depreciation'] = trim($this->findCsvMatch($row, 'depreciation'));
$this->item['requestable'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'requestable'))) == 1) ? 1 : 0;
$this->item['require_serial'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'require_serial'))) == 1) ? 1 : 0;
if (!empty($this->item['category'])) {
if ($category = $this->createOrFetchCategory($this->item['category'])) {

View File

@@ -72,6 +72,7 @@ abstract class Importer
'termination_date' => 'termination date',
'warranty_months' => 'warranty',
'full_name' => 'full name',
'display_name' => 'display name',
'email' => 'email',
'username' => 'username',
'address' => 'address',
@@ -299,6 +300,7 @@ abstract class Importer
'full_name' => $this->findCsvMatch($row, 'full_name'),
'first_name' => $this->findCsvMatch($row, 'first_name'),
'last_name' => $this->findCsvMatch($row, 'last_name'),
'display_name' => $this->findCsvMatch($row, 'display_name'),
'email' => $this->findCsvMatch($row, 'email'),
'manager_id'=> '',
'department_id' => '',
@@ -369,6 +371,7 @@ abstract class Importer
$user->first_name = $user_array['first_name'];
$user->last_name = $user_array['last_name'];
$user->username = $user_array['username'];
$user->display_name = $user_array['display_name'] ?? null;
$user->email = $user_array['email'];
$user->manager_id = $user_array['manager_id'] ?? null;
$user->department_id = $user_array['department_id'] ?? null;

View File

@@ -27,7 +27,7 @@ class ManufacturerImporter extends ItemImporter
}
/**
* Create a supplier if a duplicate does not exist.
* Create a manufacturer if a duplicate does not exist.
* @todo Investigate how this should interact with Importer::createManufacturerIfNotExists
*
* @author A. Gianotto
@@ -39,16 +39,16 @@ class ManufacturerImporter extends ItemImporter
$editingManufacturer = false;
$supplier = Manufacturer::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
$manufacturer = 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'));
// Override manufacturer if an ID was given
\Log::debug('Finding manufacturer by ID: '.$this->findCsvMatch($row, 'id'));
$manufacturer = Manufacturer::find($this->findCsvMatch($row, 'id'));
}
if ($supplier) {
if ($manufacturer) {
if (! $this->updating) {
$this->log('A matching Manufacturer '.$this->item['name'].' already exists');
return;
@@ -58,8 +58,8 @@ class ManufacturerImporter extends ItemImporter
$editingManufacturer = true;
} else {
$this->log('No Matching Manufacturer, Create a new one');
$supplier = new Manufacturer;
$supplier->created_by = auth()->id();
$manufacturer = new Manufacturer;
$manufacturer->created_by = auth()->id();
}
// Pull the records from the CSV to determine their values
@@ -79,21 +79,21 @@ class ManufacturerImporter extends ItemImporter
if ($editingManufacturer) {
Log::debug('Updating existing supplier');
$supplier->update($this->sanitizeItemForUpdating($supplier));
Log::debug('Updating existing manufacturer');
$manufacturer->update($this->sanitizeItemForUpdating($manufacturer));
} else {
Log::debug('Creating supplier');
$supplier->fill($this->sanitizeItemForStoring($supplier));
Log::debug('Creating manufacturer');
$manufacturer->fill($this->sanitizeItemForStoring($manufacturer));
}
if ($supplier->save()) {
$this->log('Manufacturer '.$supplier->name.' created or updated from CSV import');
return $supplier;
if ($manufacturer->save()) {
$this->log('Manufacturer '.$manufacturer->name.' created or updated from CSV import');
return $manufacturer;
} else {
Log::debug($supplier->getErrors());
$this->logError($supplier, 'Manufacturer "'.$this->item['name'].'"');
return $supplier->errors;
Log::debug($manufacturer->getErrors());
$this->logError($manufacturer, 'Manufacturer "'.$this->item['name'].'"');
return $manufacturer->errors;
}

View File

@@ -47,6 +47,7 @@ class UserImporter extends ItemImporter
// Pull the records from the CSV to determine their values
$this->item['id'] = trim($this->findCsvMatch($row, 'id'));
$this->item['username'] = trim($this->findCsvMatch($row, 'username'));
$this->item['display_name'] = trim($this->findCsvMatch($row, 'display_name'));
$this->item['first_name'] = trim($this->findCsvMatch($row, 'first_name'));
$this->item['last_name'] = trim($this->findCsvMatch($row, 'last_name'));
$this->item['email'] = trim($this->findCsvMatch($row, 'email'));

View File

@@ -96,7 +96,8 @@ class CheckoutableListener
if (!empty($to)) {
try {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable);
$toMail = (clone $mailable)->locale($notifiable->locale);
Mail::to(array_flatten($to))->send($toMail);
Log::info('Checkout Mail sent to checkout target');
} catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
@@ -104,6 +105,16 @@ class CheckoutableListener
Log::debug("Exception caught during checkout email: " . $e->getMessage());
}
}
if (!empty($cc)) {
try {
$ccMail = (clone $mailable)->locale(Setting::getSettings()->locale);
Mail::to(array_flatten($cc))->send($ccMail);
} catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
}
}
}
if ($shouldSendWebhookNotification) {
@@ -178,15 +189,26 @@ class CheckoutableListener
[$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
try {
if (!empty($to)) {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable);
Log::info('Checkin Mail sent to CC addresses');
if (!empty($to)) {
try {
$toMail = (clone $mailable)->locale($notifiable->locale);
Mail::to(array_flatten($to))->send($toMail);
Log::info('Checkin Mail sent to checkin 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());
}
}
if (!empty($cc)) {
try {
$ccMail = (clone $mailable)->locale(Setting::getSettings()->locale);
Mail::to(array_flatten($cc))->send($ccMail);
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
}
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
}
}
@@ -240,6 +262,12 @@ class CheckoutableListener
$acceptance->checkoutable()->associate($event->checkoutable);
$acceptance->assignedTo()->associate($event->checkedOutTo);
$acceptance->qty = 1;
if (isset($event->checkoutable->checkout_qty)) {
$acceptance->qty = $event->checkoutable->checkout_qty;
}
$category = $this->getCategoryFromCheckoutable($event->checkoutable);
if ($category?->alert_on_response) {

View File

@@ -339,6 +339,7 @@ class Importer extends Component
'start_date' => trans('general.start_date'),
'state' => trans('general.state'),
'username' => trans('admin/users/table.username'),
'display_name' => trans('admin/users/table.display_name'),
'vip' => trans('general.importer.vip'),
'website' => trans('general.website'),
'zip' => trans('general.zip'),
@@ -402,6 +403,7 @@ class Importer extends Component
$this->assetmodels_fields = [
'id' => trans('general.id'),
'category' => trans('general.category'),
'eol' => trans('general.eol'),
'fieldset' => trans('admin/models/general.fieldset'),
@@ -411,6 +413,7 @@ class Importer extends Component
'model_number' => trans('general.model_no'),
'notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]),
'requestable' => trans('admin/models/general.requestable'),
'require_serial' => trans('admin/hardware/general.require_serial'),
];
@@ -485,6 +488,13 @@ class Importer extends Component
'username',
trans('general.importer.checked_out_to_username'),
],
'display_name' =>
[
'display name',
'displayName',
'display',
trans('admin/users/table.display_name'),
],
'first_name' =>
[
'first name',
@@ -527,6 +537,10 @@ class Importer extends Component
'product key',
'key',
],
'require_serial' =>
[
'serial required',
],
'model_number' =>
[
'model',

View File

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

View File

@@ -3,6 +3,8 @@
namespace App\Mail;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
@@ -41,7 +43,7 @@ class CheckoutAccessoryMail extends Mailable
return new Envelope(
from: $from,
subject: (trans('mail.Accessory_Checkout_Notification')),
subject: trans_choice('mail.Accessory_Checkout_Notification', $this->checkout_qty),
);
}
@@ -54,6 +56,17 @@ class CheckoutAccessoryMail extends Mailable
$eula = $this->item->getEula();
$req_accept = $this->item->requireAcceptance();
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
$name = null;
if($this->target instanceof User){
$name = $this->target->display_name;
}
else if($this->target instanceof Asset){
$name = $this->target->assignedto?->display_name;
}
else if($this->target instanceof Location){
$name = $this->target->manager->name;
}
return new Content(
markdown: 'mail.markdown.checkout-accessory',
@@ -61,15 +74,38 @@ class CheckoutAccessoryMail extends Mailable
'item' => $this->item,
'admin' => $this->admin,
'note' => $this->note,
'target' => $this->target,
'target' => $name,
'eula' => $eula,
'req_accept' => $req_accept,
'accept_url' => $accept_url,
'checkout_qty' => $this->checkout_qty,
'introduction_line' => $this->introductionLine(),
],
);
}
private function introductionLine(): string
{
if ($this->target instanceof Location) {
return trans_choice('mail.new_item_checked_location', $this->checkout_qty, ['location' => $this->target->name]);
}
if ($this->requiresAcceptance()) {
return trans_choice('mail.new_item_checked_with_acceptance', $this->checkout_qty);
}
if (!$this->requiresAcceptance()) {
return trans_choice('mail.new_item_checked', $this->checkout_qty);
}
// we shouldn't get here but let's send a default message just in case
return trans('new_item_checked');
}
private function requiresAcceptance(): int|bool
{
return method_exists($this->item, 'requireAcceptance') ? $this->item->requireAcceptance() : 0;
}
/**
* Get the attachments for the message.
*

View File

@@ -4,6 +4,7 @@ namespace App\Mail;
use App\Helpers\Helper;
use App\Models\Asset;
use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
@@ -36,14 +37,6 @@ class CheckoutAssetMail extends Mailable
$this->settings = Setting::getSettings();
$this->target = $checkedOutTo;
// Location is a target option, but there are no emails currently associated with locations.
if($this->target instanceof User){
$this->target = $this->target->present()?->fullName();
}
else if($this->target instanceof Asset){
$this->target = $this->target->assignedto?->present()?->fullName();
}
$this->last_checkout = '';
$this->expected_checkin = '';
@@ -85,6 +78,17 @@ class CheckoutAssetMail extends Mailable
$eula = method_exists($this->item, 'getEula') ? $this->item->getEula() : '';
$req_accept = $this->requiresAcceptance();
$fields = [];
$name = null;
if($this->target instanceof User){
$name = $this->target->display_name;
}
else if($this->target instanceof Asset){
$name = $this->target->assignedto?->display_name;
}
else if($this->target instanceof Location){
$name = $this->target->manager->name;
}
// Check if the item has custom fields associated with it
if (($this->item->model) && ($this->item->model->fieldset)) {
@@ -100,7 +104,7 @@ class CheckoutAssetMail extends Mailable
'admin' => $this->admin,
'status' => $this->item->assetstatus?->name,
'note' => $this->note,
'target' => $this->target,
'target' => $name,
'fields' => $fields,
'eula' => $eula,
'req_accept' => $req_accept,
@@ -125,7 +129,7 @@ class CheckoutAssetMail extends Mailable
private function getSubject(): string
{
if ($this->firstTimeSending) {
return trans('mail.Asset_Checkout_Notification');
return trans('mail.Asset_Checkout_Notification', ['tag' => $this->item->asset_tag]);
}
return trans('mail.unaccepted_asset_reminder');
@@ -133,12 +137,16 @@ class CheckoutAssetMail extends Mailable
private function introductionLine(): string
{
if ($this->firstTimeSending && $this->target instanceof Location) {
return trans_choice('mail.new_item_checked_location', 1, ['location' => $this->target->name]);
}
if ($this->firstTimeSending && $this->requiresAcceptance()) {
return trans('mail.new_item_checked_with_acceptance');
return trans_choice('mail.new_item_checked_with_acceptance', 1);
}
if ($this->firstTimeSending && !$this->requiresAcceptance()) {
return trans('mail.new_item_checked');
return trans_choice('mail.new_item_checked', 1);
}
if (!$this->firstTimeSending && $this->requiresAcceptance()) {

View File

@@ -26,7 +26,7 @@ class CheckoutComponentMail extends Mailable
$this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->qty = $component->assets->first()?->pivot?->assigned_qty;
$this->qty = $component->checkout_qty;
$this->settings = Setting::getSettings();
}

View File

@@ -31,10 +31,10 @@ class CheckoutLicenseMail extends Mailable
$this->target = $checkedOutTo;
if($this->target instanceof User){
$this->target = $this->target->present()?->fullName();
$this->target = $this->target->display_name;
}
elseif($this->target instanceof Asset){
$this->target = $this->target->assignedto?->present()?->fullName();
$this->target = $this->target->display_name;
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\Acceptable;
use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
@@ -65,7 +66,7 @@ class Accessory extends SnipeModel
'company_id' => 'integer|nullable',
'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|nullable',
'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',
'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'purchase_date' => 'date_format:Y-m-d|nullable',
];
@@ -308,27 +309,6 @@ class Accessory extends SnipeModel
return $this->category->require_acceptance ?? false;
}
/**
* Checks for a category-specific EULA, and if that doesn't exist,
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return string
*/
public function getEula()
{
if ($this->category->eula_text) {
return Helper::parseEscapedMarkedown($this->category->eula_text);
} elseif ((Setting::getSettings()->default_eula_text) && ($this->category->use_default_eula == '1')) {
return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
}
return null;
}
/**
* Check how many items within an accessory are checked out
*
@@ -377,6 +357,10 @@ class Accessory extends SnipeModel
$accessory_checkout->limit(1)->delete();
}
public function totalCostSum() {
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
}
/**
* -----------------------------------------------

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Carbon\Carbon;
@@ -245,19 +246,6 @@ class Actionlog extends SnipeModel
}
/**
* Establishes the actionlog -> uploads relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function uploads()
{
return $this->morphTo('item')
->where('action_type', '=', 'uploaded')
->withTrashed();
}
/**
* Establishes the actionlog -> userlog relationship
@@ -455,6 +443,26 @@ class Actionlog extends SnipeModel
}
/**
* @author Godfrey Martinez
* @since [v8.0.4]
* @return \App\Models\Actionlog
*/
public function logUploadDelete($object, $filename)
{
$log = new Actionlog;
$log->item_type = $object instanceof SnipeModel ? get_class($object) : $object;
$log->item_id = $object->id;
$log->created_by = auth()->id();
$log->target_id = null;
$log->filename = $filename;
$log->created_at = date('Y-m-d H:i:s');
$log->logaction('upload deleted');
return $log;
}
public function uploads_file_url()
{

View File

@@ -7,19 +7,20 @@ use App\Exceptions\CheckoutNotAllowed;
use App\Helpers\Helper;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\Acceptable;
use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use App\Presenters\AssetPresenter;
use App\Presenters\Presentable;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* Model for Assets.
@@ -113,7 +114,7 @@ class Asset extends Depreciable
'rtd_location_id' => ['nullable', 'exists:locations,id', 'fmcs_location'],
'purchase_date' => ['nullable', 'date', 'date_format:Y-m-d'],
'serial' => ['nullable', 'string', 'unique_undeleted:assets,serial'],
'purchase_cost' => ['nullable', 'numeric', 'gte:0', 'max:9999999999999'],
'purchase_cost' => ['nullable', 'numeric', 'gte:0', 'max:99999999999999999.99'],
'supplier_id' => ['nullable', 'exists:suppliers,id'],
'asset_eol_date' => ['nullable', 'date'],
'eol_explicit' => ['nullable', 'boolean'],
@@ -160,7 +161,6 @@ class Asset extends Depreciable
'eol_explicit',
'last_audit_date',
'next_audit_date',
'asset_eol_date',
'last_checkin',
'last_checkout',
];
@@ -823,21 +823,26 @@ class Asset extends Depreciable
* @since [v2.0]
* @return mixed
*/
public static function getExpiringWarrantee($days = 30)
public static function getExpiringWarrantyOrEol($days = 30)
{
$days = (is_null($days)) ? 30 : $days;
return self::where('archived', '=', '0') // this can stay for right now, as `archived` defaults to 0 at the db level, but should probably be replaced with assetstatus->archived?
->whereNotNull('warranty_months')
->whereNotNull('purchase_date')
->whereNull('deleted_at')
return self::where('archived', '=', '0')
->NotArchived()
->whereRaw(
'DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) <= DATE_ADD(NOW(), INTERVAL '
. $days
. ' DAY) AND DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) > NOW()'
)
->orderByRaw('DATE_ADD(`purchase_date`,INTERVAL `warranty_months` MONTH)')
->whereNull('deleted_at')
->where(function ($query) use ($days) {
// Check for manual asset EOL first
$query->where(function ($query) use ($days) {
$query->whereNotNull('asset_eol_date')
->whereBetween('asset_eol_date', [Carbon::now(), Carbon::now()->addDays($days)]);
// Otherwise use the warranty months + purchase date + threshold
})->orWhere(function ($query) use ($days) {
$query->whereNotNull('purchase_date')
->whereNotNull('warranty_months')
->whereBetween('purchase_date', [Carbon::now(), Carbon::now()->addMonths('assets.warranty_months')->addDays($days)]);
});
})
->orderBy('asset_eol_date', 'ASC')
->orderBy('purchase_date', 'ASC')
->get();
}
@@ -1018,31 +1023,6 @@ class Asset extends Depreciable
return false;
}
/**
* Checks for a category-specific EULA, and if that doesn't exist,
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return string | false
*/
public function getEula()
{
if (($this->model) && ($this->model->category)) {
if (($this->model->category->eula_text) && ($this->model->category->use_default_eula === 0)) {
return Helper::parseEscapedMarkedown($this->model->category->eula_text);
} elseif ($this->model->category->use_default_eula === 1) {
return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
} else {
return false;
}
}
return false;
}
public function getComponentCost()
{
$cost = 0;
@@ -1892,6 +1872,30 @@ class Asset extends Depreciable
);
}
if ($fieldname == 'jobtitle') {
$query->where(function ($query) use ($search_val) {
if (is_array($search_val)) {
$query->whereHasMorph(
'assignedTo',
[User::class],
function ($query) use ($search_val) {
$query->whereIn('users.jobtitle', $search_val);
}
);
} else {
$query->whereHasMorph(
'assignedTo',
[User::class],
function ($query) use ($search_val) {
$query->where(function ($query) use ($search_val) {
$query->where('users.jobtitle', 'LIKE', '%' . $search_val . '%');
});
}
);
}
});
}
/**
* THIS CLUNKY BIT IS VERY IMPORTANT
@@ -1916,7 +1920,7 @@ class Asset extends Depreciable
*/
if (($fieldname!='category') && ($fieldname!='model_number') && ($fieldname!='rtd_location') && ($fieldname!='location') && ($fieldname!='supplier')
&& ($fieldname!='status_label') && ($fieldname!='assigned_to') && ($fieldname!='model') && ($fieldname!='company') && ($fieldname!='manufacturer')
&& ($fieldname!='status_label') && ($fieldname!='assigned_to') && ($fieldname!='model') && ($fieldname!='jobtitle') && ($fieldname!='company') && ($fieldname!='manufacturer')
) {
$query->where('assets.'.$fieldname, 'LIKE', '%' . $search_val . '%');
}

View File

@@ -71,6 +71,7 @@ class AssetModel extends SnipeModel
'name',
'notes',
'requestable',
'require_serial'
];
use Searchable;

View File

@@ -2,11 +2,14 @@
namespace App\Models;
use App\Helpers\Helper;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Notifications\Notifiable;
use TCPDF;
class CheckoutAcceptance extends Model
{
@@ -32,7 +35,19 @@ class CheckoutAcceptance extends Model
return array_filter($recipients);
}
public function getCheckoutableItemTypeAttribute(): string
{
$type = $this->checkoutable_type;
return match ($type) {
Asset::class => trans('general.asset'),
LicenseSeat::class => trans('general.license'),
Accessory::class => trans('general.accessory'),
Component::class => trans('general.component'),
Consumable::class => trans('general.consumable'),
default => class_basename($type),
};
}
/**
* The resource that was is out
*
@@ -117,8 +132,7 @@ class CheckoutAcceptance extends Model
/**
* Filter checkout acceptences by the user
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param User $user
* @param User $user
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForUser(Builder $query, User $user)
@@ -129,11 +143,111 @@ class CheckoutAcceptance extends Model
/**
* Filter to only get pending acceptances
*
* @param Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePending(Builder $query)
{
return $query->whereNull('accepted_at')->whereNull('declined_at');
}
public function scopeDeclined(Builder $query)
{
return $query->whereNull('accepted_at')->whereNotNull('declined_at');
}
protected function displayCheckoutableType(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => strtolower(str_replace('App\Models\\', '', $this->checkoutable_type)),
);
}
public function generateAcceptancePdf($data, $pdf_filename) {
// set some language dependent data:
$lg = Array();
$lg['a_meta_charset'] = 'UTF-8';
$lg['w_page'] = 'page';
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->setRTL(false);
$pdf->setLanguageArray($lg);
$pdf->SetFontSubsetting(true);
$pdf->SetCreator('Snipe-IT Asset Management System');
$pdf->SetAuthor($data['assigned_to']);
$pdf->SetTitle('Asset Acceptance: '.$data['item_tag']);
$pdf->SetSubject('Asset Acceptance: '.$data['item_tag']);
$pdf->SetKeywords('Snipe-IT, assets, acceptance, eula, tos');
$pdf->SetFont('dejavusans', '', 8, '', true);
$pdf->SetPrintHeader(false);
$pdf->SetPrintFooter(false);
$pdf->AddPage();
if ($data['logo'] != null) {
$pdf->writeHTML('<img src="'.$data['logo'].'">', true, 0, true, 0, '');
} else {
$pdf->writeHTML('<h3>'.$data['site_name'].'</h3><br /><br />', true, 0, true, 0, 'C');
}
$pdf->Ln();
$pdf->writeHTML(trans('general.date') . ': ' . Helper::getFormattedDateObject(now(), 'datetime', false), true, 0, true, 0, '');
if ($data['company_name'] != null) {
$pdf->writeHTML(trans('general.company') . ': ' . e($data['company_name']), true, 0, true, 0, '');
}
if ($data['item_tag'] != null) {
$pdf->writeHTML(trans('general.asset_tag') . ': ' . e($data['item_tag']), true, 0, true, 0, '');
}
if ($data['item_name'] != null) {
$pdf->writeHTML(trans('general.name') . ': ' . e($data['item_name']), true, 0, true, 0, '');
}
if ($data['item_model'] != null) {
$pdf->writeHTML(trans('general.asset_model') . ': ' . e($data['item_model']), true, 0, true, 0, '');
}
if ($data['item_serial'] != null) {
$pdf->writeHTML(trans('admin/hardware/form.serial').': '.e($data['item_serial']), true, 0, true, 0, '');
}
if (($data['qty'] != null) && ($data['qty'] > 1)) {
$pdf->writeHTML(trans('general.qty').': '.e($data['qty']), true, 0, true, 0, '');
}
$pdf->writeHTML(trans('general.assignee').': '.e($data['assigned_to']), true, 0, true, 0, '');
$pdf->Ln();
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
// Break the EULA into lines based on newlines, and check each line for RTL or CJK characters
$eula_lines = preg_split("/\r\n|\n|\r/", $data['eula']);
foreach ($eula_lines as $eula_line) {
Helper::hasRtl($eula_line) ? $pdf->setRTL(true) : $pdf->setRTL(false);
Helper::isCjk($eula_line) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
$pdf->writeHTML(Helper::parseEscapedMarkedown($eula_line), true, 0, true, 0, '');
}
$pdf->Ln();
$pdf->Ln();
$pdf->setRTL(false);
$pdf->Ln();
if ($data['signature'] != null) {
$pdf->writeHTML('<img src="'.$data['signature'].'">', true, 0, true, 0, '');
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
$pdf->writeHTML(e($data['assigned_to']), true, 0, true, 0, 'C');
$pdf->Ln();
}
if ($data['note'] != null) {
Helper::isCjk($data['note']) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
$pdf->writeHTML(trans('general.notes') . ': ' . e($data['note']), true, 0, true, 0, '');
$pdf->Ln();
}
$pdf->writeHTML(trans('general.assigned_date').': '.e($data['check_out_date']), true, 0, true, 0, '');
$pdf->writeHTML(trans('general.accepted_date').': '.e($data['accepted_date']), true, 0, true, 0, '');
return $pdf->Output($pdf_filename, 'S');
}
}

View File

@@ -46,7 +46,7 @@ class CheckoutRequest extends Model
public function name()
{
if ($this->itemType() == 'asset') {
return $this->itemRequested()->present()->name();
return $this->itemRequested()->display_name;
}
return $this->itemRequested()->name;

View File

@@ -2,14 +2,16 @@
namespace App\Models;
use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Watson\Validating\ValidatingTrait;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Watson\Validating\ValidatingTrait;
/**
* Model for Companies.
*

View File

@@ -3,13 +3,13 @@
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
/**
@@ -43,7 +43,7 @@ class Component extends SnipeModel
'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|nullable',
'purchase_date' => 'date_format:Y-m-d|nullable',
'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',
'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'manufacturer_id' => 'integer|exists:manufacturers,id|nullable',
];
@@ -217,24 +217,6 @@ class Component extends SnipeModel
return $this->category->require_acceptance;
}
/**
* Checks for a category-specific EULA, and if that doesn't exist,
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return string | false
*/
public function getEula()
{
if ($this->category->eula_text) {
return Helper::parseEscapedMarkedown($this->category->eula_text);
} elseif ((Setting::getSettings()->default_eula_text) && ($this->category->use_default_eula == '1')) {
return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
} else {
return null;
}
}
/**
* Establishes the component -> action logs relationship
@@ -306,7 +288,10 @@ class Component extends SnipeModel
return $this->qty - $this->numCheckedOut();
}
public function totalCostSum() {
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS

View File

@@ -4,22 +4,16 @@ namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\Acceptable;
use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\ConsumablePresenter;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Relations\Relation;
use App\Presenters\ConsumablePresenter;
use App\Models\Actionlog;
use App\Models\ConsumableAssignment;
use App\Models\User;
use App\Models\Location;
use App\Models\Manufacturer;
use App\Models\Supplier;
use App\Models\Category;
class Consumable extends SnipeModel
{
@@ -53,7 +47,7 @@ class Consumable extends SnipeModel
'company_id' => 'integer|nullable',
'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|max:99999|nullable',
'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',
'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'purchase_date' => 'date_format:Y-m-d|nullable',
];
@@ -291,25 +285,6 @@ class Consumable extends SnipeModel
return $this->category->require_acceptance;
}
/**
* Checks for a category-specific EULA, and if that doesn't exist,
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return string | false
*/
public function getEula()
{
if ($this->category->eula_text) {
return Helper::parseEscapedMarkedown($this->category->eula_text);
} elseif ((Setting::getSettings()->default_eula_text) && ($this->category->use_default_eula == '1')) {
return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
} else {
return null;
}
}
/**
* Check how many items within a consumable are checked out
*
@@ -337,7 +312,10 @@ class Consumable extends SnipeModel
return $remaining;
}
public function totalCostSum() {
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Models\Traits\CompanyableTrait;
use Illuminate\Database\Eloquent\Model;
use Watson\Validating\ValidatingTrait;

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