Merge branch 'develop' into improve_javascript

This commit is contained in:
Brady Wetherington 2024-09-03 14:30:21 +01:00
commit d43824c73e
1158 changed files with 9543 additions and 4223 deletions

View file

@ -3172,6 +3172,51 @@
"contributions": [
"code"
]
},
{
"login": "arne-kroeger",
"name": "arne-kroeger",
"avatar_url": "https://avatars.githubusercontent.com/u/65785975?v=4",
"profile": "https://github.com/arne-kroeger",
"contributions": [
"code"
]
},
{
"login": "Glukose1",
"name": "Glukose1",
"avatar_url": "https://avatars.githubusercontent.com/u/167117705?v=4",
"profile": "https://github.com/Glukose1",
"contributions": [
"code"
]
},
{
"login": "Scarzy",
"name": "Scarzy",
"avatar_url": "https://avatars.githubusercontent.com/u/1197791?v=4",
"profile": "https://github.com/Scarzy",
"contributions": [
"code"
]
},
{
"login": "setpill",
"name": "setpill",
"avatar_url": "https://avatars.githubusercontent.com/u/37372069?v=4",
"profile": "https://github.com/setpill",
"contributions": [
"code"
]
},
{
"login": "swift2512",
"name": "swift2512",
"avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4",
"profile": "https://github.com/swift2512",
"contributions": [
"bug"
]
}
]
}

View file

@ -1,6 +1,8 @@
# --------------------------------------------
# REQUIRED: DB SETUP
# --------------------------------------------
# https://mariadb.com/kb/en/mariadb-server-docker-official-image-environment-variables/
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234

View file

@ -32,6 +32,8 @@ DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
DB_SANITIZE_BY_DEFAULT=false
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS

43
.github/stale.yml vendored
View file

@ -1,43 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- :woman_technologist: ready for dev
- :moneybag: bounty
- :hand: bug
- "🔐 security"
- "👩‍💻 ready for dev"
- "💰 bounty"
- "✋ bug"
exemptMilestones: true
# Label to use when marking an issue as stale
staleLabel: stale
only: issues
# Comment to post when removing the stale label.
unmarkComment: >
Okay, it looks like this issue or feature request might still be important. We'll re-open
it for now. Thank you for letting us know!
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
Is this still relevant? We haven't heard from anyone in a bit. If so,
please comment with any updates or additional detail.
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Don't
take it personally, we just need to keep a handle on things. Thank you
for your contributions!
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been automatically closed because it has not had
recent activity. If you believe this is still an issue, please confirm that
this issue is still happening in the most recent version of Snipe-IT and reply
to this thread to re-open it.

39
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,39 @@
name: 'Close stale issues'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
# contents: write # only for delete-branch option
issues: write
# pull-requests: write
steps:
- uses: actions/stale@v9
with:
debug-only: true
operations-per-run: 100 # just while we're debugging
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
days-before-close: 7
exempt-all-milestones: true
stale-issue-message: >
Is this still relevant? We haven't heard from anyone in a bit. If so,
please comment with any updates or additional detail.
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Don't
take it personally, we just need to keep a handle on things. Thank you
for your contributions!
close-issue-message: >
This issue has been automatically closed because it has not had
recent activity. If you believe this is still an issue, please confirm that
this issue is still happening in the most recent version of Snipe-IT and reply
to this thread to re-open it.
# There doesn't seem to be a 'reopen issue message'?
# Since there is no 'stale-pr-message' - PR's should not be stale'd
stale-issue-label: stale
exempt-issue-labels: >
pinned,security,:woman_technologist: ready for dev,:moneybag: bounty,:hand: bug,🔐 security,👩‍💻 ready for dev,💰 bounty,✋ bug

2
.gitignore vendored
View file

@ -68,3 +68,5 @@ _ide_helper_models.php
storage/ldap_client_tls.cert
storage/ldap_client_tls.key
/storage/framework/testing
/.phpunit.cache

View file

@ -51,7 +51,8 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") | [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") | [<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/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") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View file

@ -1,35 +1,35 @@
FROM alpine:3.18.6
FROM alpine:3.19
# Apache + PHP
RUN apk add --no-cache \
apache2 \
php81 \
php81-common \
php81-apache2 \
php81-curl \
php81-ldap \
php81-mysqli \
php81-gd \
php81-xml \
php81-mbstring \
php81-zip \
php81-ctype \
php81-tokenizer \
php81-pdo_mysql \
php81-openssl \
php81-bcmath \
php81-phar \
php81-json \
php81-iconv \
php81-fileinfo \
php81-simplexml \
php81-session \
php81-dom \
php81-xmlwriter \
php81-xmlreader \
php81-sodium \
php81-redis \
php81-pecl-memcached \
php81-exif \
php82 \
php82-common \
php82-apache2 \
php82-curl \
php82-ldap \
php82-mysqli \
php82-gd \
php82-xml \
php82-mbstring \
php82-zip \
php82-ctype \
php82-tokenizer \
php82-pdo_mysql \
php82-openssl \
php82-bcmath \
php82-phar \
php82-json \
php82-iconv \
php82-fileinfo \
php82-simplexml \
php82-session \
php82-dom \
php82-xmlwriter \
php82-xmlreader \
php82-sodium \
php82-redis \
php82-pecl-memcached \
php82-exif \
curl \
wget \
vim \
@ -42,7 +42,7 @@ COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
# Where apache's PID lives
RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php81/php.ini
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php82/php.ini
COPY docker/000-default-2.4.conf /etc/apache2/conf.d/default.conf
# Enable mod_rewrite
@ -79,12 +79,12 @@ USER root
VOLUME ["/var/lib/snipeit"]
# Entrypoints
COPY docker/entrypoint_alpine.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Startup script
COPY docker/startup_alpine.sh /startup.sh
RUN chmod +x /startup.sh
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/entrypoint.sh"]
CMD ["/startup.sh"]
EXPOSE 80

View file

@ -97,7 +97,7 @@ RUN set -eux; \
VOLUME [ "/var/lib/snipeit" ]
COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env
COPY --chmod=655 docker/docker-entrypoint.sh /usr/local/bin/docker-snipeit-entrypoint
COPY --chmod=655 docker/startup_alpine_fpm.sh /startup.sh
COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
ENTRYPOINT [ "/usr/local/bin/docker-snipeit-entrypoint" ]
CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]
ENTRYPOINT [ "/startup.sh" ]
CMD [ "/startup.sh", "php-fpm" ]

View file

@ -72,12 +72,13 @@ Since the release of the JSON REST API, several third-party developers have been
- [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira)
- [Python 3 CSV importer](https://github.com/gastamper/snipeit-csvimporter) - allows importing assets into Snipe-IT based on Item Name rather than Asset Tag.
- [Snipe-IT Kubernetes Helm Chart](https://github.com/t3n/helm-charts/tree/master/snipeit) - For more information, [click here](https://hub.helm.sh/charts/t3n/snipeit).
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-it.
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-IT.
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT.
- [WWW::SnipeIT](https://github.com/SEDC/perl-www-snipeit) by [@SEDC](https://github.com/SEDC) - perl module for accessing the API
- [UniFi to Snipe-IT](https://github.com/RodneyLeeBrands/UnifiSnipeSync) by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
- [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT.
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by @ReticentRobot - Windows agent for Snipe-IT
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by [@ReticentRobot](https://github.com/ReticentRobot) - Windows agent for Snipe-IT.
- [Gate Pass Generator](https://github.com/cha7uraAE/snipe-it-gate-pass-system) by [@cha7uraAE](https://github.com/cha7uraAE) - A Streamlit application for generating gate passes based on hardware data from a Snipe-IT API.
-----

View file

@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use Illuminate\Console\Command;
class FixupAssignedToWithoutAssignedType extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:assigned-to-fixup
{--debug : Display debugging output}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes up assets that have an assigned_to but no assigned_type';
/**
* Execute the console command.
*/
public function handle()
{
$assets = Asset::whereNull("assigned_type")->whereNotNull("assigned_to")->withTrashed();
$this->withProgressBar($assets->get(), function (Asset $asset) {
//now check each action log, from the most recent backwards, to find the last checkin or checkout
foreach($asset->log()->orderBy("id","desc")->get() as $action_log) {
if($this->option("debug")) {
$this->info("Asset id: " . $asset->id . " action log, action type is: " . $action_log->action_type);
}
switch($action_log->action_type) {
case 'checkin from':
if($this->option("debug")) {
$this->info("Doing a checkin for ".$asset->id);
}
$asset->assigned_to = null;
// if you have a required custom field, we still want to save, and we *don't* want an action_log
$asset->saveQuietly();
return;
case 'checkout':
if($this->option("debug")) {
$this->info("Doing a checkout for " . $asset->id . " picking target type: " . $action_log->target_type);
}
if($asset->assigned_to != $action_log->target_id) {
$this->error("Asset's assigned_to does *NOT* match Action Log's target_id. \$asset->assigned_to=".$asset->assigned_to." vs. \$action_log->target_id=".$action_log->target_id);
//FIXME - do we abort here? Do we try to keep looking? I don't know, this means your data is *really* messed up...
}
$asset->assigned_type = $action_log->target_type;
$asset->saveQuietly(); // see above
return;
}
}
$asset->assigned_to = null; //asset was never checked in or out in its lifetime - it stays 'checked in'
$asset->saveQuietly(); //see above
});
$this->newLine();
$this->info("Assets assigned_type are fixed");
}
}

View file

@ -3,6 +3,7 @@
namespace App\Console\Commands;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Category;
@ -15,6 +16,8 @@ use App\Models\Statuslabel;
use App\Models\Supplier;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class Purge extends Command
{
@ -141,6 +144,20 @@ class Purge extends Command
$this->info($users->count().' users purged.');
$user_assoc = 0;
foreach ($users as $user) {
$rel_path = 'private_uploads/users';
$filenames = Actionlog::where('action_type', 'uploaded')
->where('item_id', $user->id)
->pluck('filename');
foreach($filenames as $filename) {
try {
if (Storage::exists($rel_path . '/' . $filename)) {
Storage::delete($rel_path . '/' . $filename);
}
} catch (\Exception $e) {
Log::info('An error occurred while deleting files: ' . $e->getMessage());
}
}
$this->info('- User "'.$user->username.'" deleted.');
$user_assoc += $user->userlog()->count();
$user->userlog()->forceDelete();

View file

@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\AssetModel;
use Illuminate\Console\Command;
class RemoveExplicitEols extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:remove-explicit-eols {--model_name= : The name of the asset model to update (use "all" to update all models)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes explicit EOLs on assets with selected model so they may inherit the asset model EOL';
/**
* Execute the console command.
*/
public function handle()
{
$startTime = microtime(true);
if ($this->option('model_name') == 'all') {
$assets = Asset::all();
$this->updateAssets($assets);
} else {
$assetModel = AssetModel::where('name', '=', $this->option('model_name'))->first();
if ($assetModel) {
$assets = Asset::where('model_id', '=', $assetModel->id)->get();
$this->updateAssets($assets);
} else {
$this->error('Asset model not found');
}
}
$endTime = microtime(true);
$executionTime = ($endTime - $startTime);
$this->info('Command executed in ' . round($executionTime, 2) . ' seconds.');
}
private function updateAssets($assets)
{
foreach ($assets as $asset) {
$asset->eol_explicit = 0;
$asset->asset_eol_date = null;
$asset->save();
}
$this->info($assets->count() . ' Assets updated successfully');
}
}

View file

@ -92,7 +92,7 @@ class SQLStreamer {
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_users' table?
//can't use 'users' because the 'accessories_checkout' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {

View file

@ -47,9 +47,10 @@ class SendAcceptanceReminder extends Command
{
$pending = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')
->whereHas('checkoutable', function($query) {
$query->where('archived', 0);
$query->where('accepted_at', null)
->where('declined_at', null);
})
->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.adminuser'])
->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.admin'])
->get();
$count = 0;

View file

@ -721,7 +721,7 @@ class Helper
{
$alert_threshold = \App\Models\Setting::getSettings()->alert_threshold;
$consumables = Consumable::withCount('consumableAssignments as consumable_assignments_count')->whereNotNull('min_amt')->get();
$accessories = Accessory::withCount('users as users_count')->whereNotNull('min_amt')->get();
$accessories = Accessory::withCount('checkouts as checkouts_count')->whereNotNull('min_amt')->get();
$components = Component::whereNotNull('min_amt')->get();
$asset_models = AssetModel::where('min_amt', '>', 0)->get();
$licenses = License::where('min_amt', '>', 0)->get();
@ -749,7 +749,7 @@ class Helper
}
foreach ($accessories as $accessory) {
$avail = $accessory->qty - $accessory->users_count;
$avail = $accessory->qty - $accessory->checkouts_count;
if ($avail < ($accessory->min_amt) + $alert_threshold) {
if ($accessory->qty > 0) {
$percent = number_format((($avail / $accessory->qty) * 100), 0);

190
app/Helpers/IconHelper.php Normal file
View file

@ -0,0 +1,190 @@
<?php
namespace App\Helpers;
class IconHelper
{
public static function icon($type) {
switch ($type) {
case 'checkout':
return 'fa-solid fa-rotate-left';
case 'checkin':
return 'fa-solid fa-rotate-right';
case 'edit':
return 'fas fa-pencil-alt';
case 'clone':
return 'far fa-clone';
case 'delete':
return 'fas fa-trash';
case 'create':
return 'fa-solid fa-plus';
case 'audit':
return 'fa-solid fa-clipboard-check';
case '2fa reset':
return 'fa-solid fa-mobile-screen';
case 'new-user':
return 'fa-solid fa-user-plus';
case 'merged-user':
return 'fa-solid fa-people-arrows';
case 'delete-user':
return 'fa-solid fa-user-minus';
case 'update-user':
return 'fa-solid fa-user-pen';
case 'user':
return 'fa-solid fa-user';
case 'users':
return 'fas fa-users';
case 'restore':
return 'fa-solid fa-trash-arrow-up';
case 'external-link':
return 'fa fa-external-link';
case 'email':
return 'fa-regular fa-envelope';
case 'phone':
return 'fa-solid fa-phone';
case 'long-arrow-right':
return 'fas fa-long-arrow-alt-right';
case 'download':
return 'fas fa-download';
case 'checkmark':
return 'fas fa-check icon-white';
case 'x':
return 'fas fa-times';
case 'logout':
return 'fa fa-sign-out';
case 'admin-settings':
return 'fas fa-cogs';
case 'settings':
return 'fas fa-cog';
case 'angle-left':
return 'fas fa-angle-left';
case 'warning':
return 'fas fa-exclamation-triangle';
case 'kits':
return 'fas fa-object-group';
case 'assets':
case 'asset':
return 'fas fa-barcode';
case 'accessories':
case 'accessory':
return 'far fa-keyboard';
case 'components':
case 'component':
return 'far fa-hdd';
case 'consumables':
case 'consumable':
return 'fas fa-tint';
case 'licenses':
case 'license':
return 'far fa-save';
case 'requestable':
return 'fas fa-laptop';
case 'reports':
return 'fas fa-chart-bar';
case 'heart':
return 'fas fa-heart';
case 'circle':
return 'fa-regular fa-circle';
case 'circle-solid':
return 'fa-solid fa-circle';
case 'due':
return 'fas fa-history';
case 'import':
return 'fas fa-cloud-upload-alt';
case 'search':
return 'fas fa-search';
case 'alerts':
return 'far fa-flag';
case 'password':
return 'fa-solid fa-key';
case 'api-key':
return 'fa-solid fa-user-secret';
case 'nav-toggle':
return 'fas fa-bars';
case 'dashboard':
return 'fas fa-tachometer-alt';
case 'info-circle':
return 'fas fa-info-circle';
case 'caret-right':
return 'fa fa-caret-right';
case 'caret-up':
return 'fa fa-caret-up';
case 'caret-down':
return 'fa fa-caret-down';
case 'arrow-circle-right':
return 'fa fa-arrow-circle-right';
case 'minus':
return 'fas fa-minus';
case 'spinner':
return 'fas fa-spinner fa-spin';
case 'copy-clipboard':
return 'fa-regular fa-clipboard';
case 'paperclip':
return 'fas fa-paperclip';
case 'files':
return 'fa-regular fa-file';
case 'more-info':
return 'far fa-life-ring';
case 'calendar':
return 'fas fa-calendar';
case 'plus':
return 'fas fa-plus';
case 'history':
return 'fas fa-history';
case 'more-files':
return 'fa-solid fa-laptop-file';
case 'maintenances':
return 'fas fa-wrench';
case 'seats':
return 'far fa-list-alt';
case 'globe-us':
return 'fas fa-globe-americas';
case 'locked':
return 'fas fa-lock';
case 'unlocked':
return 'fas fa-lock';
case 'locations':
return 'fas fa-map-marker-alt';
case 'location':
return 'fas fa-map-marker-alt';
case 'superadmin':
return 'fas fa-crown';
case 'print':
return 'fa-solid fa-print';
case 'checkin-and-delete':
return 'fa-solid fa-user-xmark';
case 'branding':
return 'fas fa-copyright';
case 'general-settings':
return 'fa-solid fa-list-check';
case 'groups':
return 'fa-solid fa-user-group';
case 'bell':
return 'fa-solid fa-bell';
case 'hashtag':
return 'fa-solid fa-hashtag';
case 'asset-tags':
return 'fas fa-list-ol';
case 'labels':
return 'fas fa-tags';
case 'ldap':
return 'fas fa-sitemap';
case 'google':
return 'fa-brands fa-google';
case 'saml':
return 'fas fa-sign-in-alt';
case 'backups':
return 'fas fa-file-archive';
case 'logins':
return 'fas fa-crosshairs';
case 'oauth':
return 'fas fa-user-secret';
case 'employee_num' :
return 'fa-regular fa-id-card';
case 'department' :
return 'fa-solid fa-building-user';
}
}
}

View file

@ -144,12 +144,12 @@ class AccessoriesController extends Controller
*/
public function update(ImageUploadRequest $request, $accessoryId = null) : RedirectResponse
{
if ($accessory = Accessory::withCount('users as users_count')->find($accessoryId)) {
if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId)) {
$this->authorize($accessory);
$validator = Validator::make($request->all(), [
"qty" => "required|numeric|min:$accessory->users_count"
"qty" => "required|numeric|min:$accessory->checkouts_count"
]);
if ($validator->fails()) {
@ -233,7 +233,7 @@ class AccessoriesController extends Controller
*/
public function show($accessoryID = null) : View | RedirectResponse
{
$accessory = Accessory::withCount('users as users_count')->find($accessoryID);
$accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryID);
$this->authorize('view', $accessory);
if (isset($accessory->id)) {
return view('accessories/view', compact('accessory'));

View file

@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -24,7 +25,7 @@ class AccessoryCheckinController extends Controller
*/
public function create($accessoryUserId = null, $backto = null) : View | RedirectResponse
{
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) {
if (is_null($accessory_user = DB::table('accessories_checkout')->find($accessoryUserId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
@ -39,16 +40,16 @@ class AccessoryCheckinController extends Controller
*
* @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param null $accessoryUserId
* @param null $accessoryCheckoutId
* @param string $backto
*/
public function store(Request $request, $accessoryUserId = null, $backto = null) : RedirectResponse
public function store(Request $request, $accessoryCheckoutId = null, $backto = null) : RedirectResponse
{
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) {
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryCheckoutId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
$accessory = Accessory::find($accessory_user->accessory_id);
$accessory = Accessory::find($accessory_checkout->accessory_id);
$this->authorize('checkin', $accessory);
@ -59,10 +60,8 @@ class AccessoryCheckinController extends Controller
}
// Was the accessory updated?
if (DB::table('accessories_users')->where('id', '=', $accessory_user->id)->delete()) {
$return_to = e($accessory_user->assigned_to);
event(new CheckoutableCheckedIn($accessory, User::find($return_to), auth()->user(), $request->input('note'), $checkin_at));
if ($accessory_checkout->delete()) {
event(new CheckoutableCheckedIn($accessory, $accessory_checkout->assignedTo, auth()->user(), $request->input('note'), $checkin_at));
session()->put(['redirect_option' => $request->get('redirect_option')]);

View file

@ -4,9 +4,11 @@ namespace App\Http\Controllers\Accessories;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
@ -16,6 +18,9 @@ use \Illuminate\Http\RedirectResponse;
class AccessoryCheckoutController extends Controller
{
use CheckInOutRequest;
/**
* Return the form to checkout an Accessory to a user.
*
@ -25,7 +30,7 @@ class AccessoryCheckoutController extends Controller
public function create($id) : View | RedirectResponse
{
if ($accessory = Accessory::withCount('users as users_count')->find($id)) {
if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($id)) {
$this->authorize('checkout', $accessory);
@ -58,30 +63,32 @@ class AccessoryCheckoutController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request
* @param int $accessory
* @param Accessory $accessory
*/
public function store(AccessoryCheckoutRequest $request, Accessory $accessory) : RedirectResponse
{
$this->authorize('checkout', $accessory);
$accessory->assigned_to = $request->input('assigned_to');
$user = User::find($request->input('assigned_to'));
$accessory->checkout_qty = $request->input('checkout_qty', 1);
$target = $this->determineCheckoutTarget();
$accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory->users()->attach($accessory->id, [
AccessoryCheckout::create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => Auth::id(),
'assigned_to' => $request->input('assigned_to'),
'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'),
]);
}
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note')));
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
// Set this as user since we only allow checkout to user for this item type
$request->request->add(['checkout_to_type' => 'user']);
$request->request->add(['assigned_user' => $user->id]);
$request->request->add(['checkout_to_type' => request('checkout_to_type')]);
$request->request->add(['assigned_user' => $target->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);

View file

@ -218,6 +218,7 @@ class AcceptanceController extends Controller
'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'),
@ -308,6 +309,7 @@ class AcceptanceController extends Controller
'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,

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest;
use App\Http\Requests\StoreAccessoryRequest;
@ -17,10 +18,12 @@ use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use App\Models\AccessoryCheckout;
class AccessoriesController extends Controller
{
use CheckInOutRequest;
/**
* Display a listing of the resource.
*
@ -48,13 +51,13 @@ class AccessoriesController extends Controller
'min_amt',
'company_id',
'notes',
'users_count',
'checkouts_count',
'qty',
];
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'users', 'location', 'supplier')
->withCount('users as users_count');
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier')
->withCount('checkouts as checkouts_count');
if ($request->filled('search')) {
$accessories = $accessories->TextSearch($request->input('search'));
@ -154,7 +157,7 @@ class AccessoriesController extends Controller
public function show($id)
{
$this->authorize('view', Accessory::class);
$accessory = Accessory::withCount('users as users_count')->findOrFail($id);
$accessory = Accessory::withCount('checkouts as checkouts_count')->findOrFail($id);
return (new AccessoriesTransformer)->transformAccessory($accessory);
}
@ -197,28 +200,23 @@ class AccessoriesController extends Controller
$offset = request('offset', 0);
$limit = request('limit', 50);
$accessory_users = $accessory->users;
$total = $accessory_users->count();
$accessory_checkouts = $accessory->checkouts;
$total = $accessory_checkouts->count();
if ($total < $offset) {
$offset = 0;
}
$accessory_users = $accessory->users()->skip($offset)->take($limit)->get();
$accessory_checkouts = $accessory->checkouts()->skip($offset)->take($limit)->get();
if ($request->filled('search')) {
$accessory_users = $accessory->users()
->where(function ($query) use ($request) {
$search_str = '%' . $request->input('search') . '%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
})
$accessory_checkouts = $accessory->checkouts()->TextSearch($request->input('search'))
->get();
$total = $accessory_users->count();
$total = $accessory_checkouts->count();
}
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory, $accessory_users, $total);
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory, $accessory_checkouts, $total);
}
@ -282,22 +280,22 @@ class AccessoriesController extends Controller
public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory)
{
$this->authorize('checkout', $accessory);
$accessory->assigned_to = $request->input('assigned_to');
$user = User::find($request->input('assigned_to'));
$target = $this->determineCheckoutTarget();
$accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory->users()->attach($accessory->id, [
AccessoryCheckout::create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => Auth::id(),
'assigned_to' => $request->input('assigned_to'),
'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'),
]);
}
// Set this value to be able to pass the qty through to the event
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note')));
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
@ -316,19 +314,19 @@ class AccessoriesController extends Controller
*/
public function checkin(Request $request, $accessoryUserId = null)
{
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) {
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
}
$accessory = Accessory::find($accessory_user->accessory_id);
$accessory = Accessory::find($accessory_checkout->accessory_id);
$this->authorize('checkin', $accessory);
$logaction = $accessory->logCheckin(User::find($accessory_user->assigned_to), $request->input('note'));
$logaction = $accessory->logCheckin(User::find($accessory_checkout->assigned_to), $request->input('note'));
// Was the accessory updated?
if (DB::table('accessories_users')->where('id', '=', $accessory_user->id)->delete()) {
if (! is_null($accessory_user->assigned_to)) {
$user = User::find($accessory_user->assigned_to);
if ($accessory_checkout->delete()) {
if (! is_null($accessory_checkout->assigned_to)) {
$user = User::find($accessory_checkout->assigned_to);
}
$data['log_id'] = $logaction->id;

View file

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

View file

@ -78,6 +78,10 @@ class AssetModelsController extends Controller
$assetmodels = $assetmodels->where('models.category_id', '=', $request->input('category_id'));
}
if ($request->filled('depreciation_id')) {
$assetmodels = $assetmodels->where('models.depreciation_id', '=', $request->input('depreciation_id'));
}
if ($request->filled('search')) {
$assetmodels->TextSearch($request->input('search'));
}

View file

@ -602,7 +602,7 @@ class AssetsController extends Controller
if ($field->field_encrypted == '1') {
Log::debug('This model field is encrypted in this fieldset.');
if (Gate::allows('admin')) {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
// If input value is null, use custom field's default value
if (($field_val == null) && ($request->has('model_id') != '')) {
@ -695,7 +695,7 @@ class AssetsController extends Controller
}
}
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
$field_val = Crypt::encrypt($field_val);
} else {
$problems_updating_encrypted_custom_fields = true;
@ -928,7 +928,7 @@ class AssetsController extends Controller
}
}
if ($request->has('status_id')) {
if ($request->filled('status_id')) {
$asset->status_id = $request->input('status_id');
}
@ -978,7 +978,7 @@ class AssetsController extends Controller
public function checkinByTag(Request $request, $tag = null) : JsonResponse
{
$this->authorize('checkin', Asset::class);
if(null == $tag && null !== ($request->input('asset_tag'))) {
if (null == $tag && null !== ($request->input('asset_tag'))) {
$tag = $request->input('asset_tag');
}
$asset = Asset::where('asset_tag', $tag)->first();

View file

@ -20,9 +20,22 @@ class DepreciationsController extends Controller
public function index(Request $request) : JsonResponse | array
{
$this->authorize('view', Depreciation::class);
$allowed_columns = ['id','name','months','depreciation_min','created_at'];
$allowed_columns = [
'id',
'name',
'months',
'depreciation_min',
'depreciation_type',
'created_at',
'assets_count',
'models_count',
'licenses_count',
];
$depreciations = Depreciation::select('id','name','months','depreciation_min','user_id','created_at','updated_at');
$depreciations = Depreciation::select('id','name','months','depreciation_min','depreciation_type','user_id','created_at','updated_at')
->withCount('assets as assets_count')
->withCount('models as models_count')
->withCount('licenses as licenses_count');
if ($request->filled('search')) {
$depreciations = $depreciations->TextSearch($request->input('search'));

View file

@ -27,7 +27,7 @@ class LicensesController extends Controller
$licenses = License::with('company', 'manufacturer', 'supplier','category', 'adminuser')->withCount('freeSeats as free_seats_count');
if ($request->filled('company_id')) {
$licenses->where('company_id', '=', $request->input('company_id'));
$licenses->where('licenses.company_id', '=', $request->input('company_id'));
}
if ($request->filled('name')) {

View file

@ -248,6 +248,7 @@ class LocationsController extends Controller
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withCount('accessories as accessories_count')
->findOrFail($id);
if (! $location->isDeletable()) {

View file

@ -246,7 +246,7 @@ class PredefinedKitsController extends Controller
$relation = $kit->models();
if ($relation->find($model_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, ['model' => 'Model already attached to kit']));
return response()->json(Helper::formatStandardApiResponse('error', null, ['model' => trans('admin/kits/general.model_already_attached')]));
}
$relation->attach($model_id, ['quantity' => $quantity]);

View file

@ -83,11 +83,19 @@ class ReportsController extends Controller
$offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
$limit = app('api_limit_value');
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
$order = ($request->input('order') == 'asc') ? 'asc' : 'desc';
switch ($request->input('sort')) {
case 'admin':
$actionlogs->OrderAdmin($order);
break;
default:
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
$actionlogs = $actionlogs->orderBy($sort, $order);
break;
}
$actionlogs = $actionlogs->orderBy($sort, $order)->skip($offset)->take($limit)->get();
$actionlogs = $actionlogs->skip($offset)->take($limit)->get();
return response()->json((new ActionlogsTransformer)->transformActionlogs($actionlogs, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
}

View file

@ -8,6 +8,7 @@ use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Http\Transformers\StatuslabelsTransformer;
use App\Models\Asset;
use App\Models\Setting;
use App\Models\Statuslabel;
use Illuminate\Http\Request;
use App\Http\Transformers\PieChartTransformer;
@ -187,8 +188,14 @@ class StatuslabelsController extends Controller
public function getAssetCountByStatuslabel() : array
{
$this->authorize('view', Statuslabel::class);
$statuslabels = Statuslabel::withCount('assets')->get();
$total = Array();
if (Setting::getSettings()->show_archived_in_list == 0 ) {
$statuslabels = Statuslabel::withCount('assets')->where('archived','0')->get();
} else {
$statuslabels = Statuslabel::withCount('assets')->get();
}
$total = [];
foreach ($statuslabels as $statuslabel) {

View file

@ -427,13 +427,10 @@ class UsersController extends Controller
* @param \Illuminate\Http\Request $request
* @param int $id
*/
public function update(SaveUserRequest $request, $id) : JsonResponse
public function update(SaveUserRequest $request, User $user): JsonResponse
{
$this->authorize('update', User::class);
if ($user = User::find($id)) {
$this->authorize('update', $user);
/**
@ -443,12 +440,10 @@ class UsersController extends Controller
*
*/
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
if ((($user->id == 1) || ($user->id == 2)) && (config('app.lock_passwords'))) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Permission denied. You cannot update user information via API on the demo.'));
}
$user->fill($request->all());
if ($user->id == $request->input('manager_id')) {
@ -473,16 +468,13 @@ class UsersController extends Controller
$user->permissions = $permissions_array;
}
// Update the location of any assets checked out to this user
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');
if ($user->save()) {
// Check if the request has groups passed and has a value, AND that the user us a superuser
if (($request->has('groups')) && (auth()->user()->isSuperUser())) {
@ -496,18 +488,10 @@ class UsersController extends Controller
// Sync the groups since the user is a superuser and the groups pass validation
$user->groups()->sync($request->input('groups'));
}
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
}
/**

View file

@ -151,17 +151,17 @@ class AssetModelsController extends Controller
$model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0');
$this->removeCustomFieldsDefaultValues($model);
$model->fieldset_id = $request->input('fieldset_id');
if ($this->shouldAddDefaultValues($request->input())) {
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))){
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
}
}
if ($model->save()) {
$this->removeCustomFieldsDefaultValues($model);
if ($this->shouldAddDefaultValues($request->input())) {
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))) {
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
}
}
if ($model->wasChanged('eol')) {
if ($model->eol > 0) {
$newEol = $model->eol;
@ -202,6 +202,7 @@ class AssetModelsController extends Controller
if ($model->image) {
try {
Storage::disk('public')->delete('models/'.$model->image);
$model->update(['image' => null]);
} catch (\Exception $e) {
Log::info($e);
}
@ -233,7 +234,7 @@ class AssetModelsController extends Controller
if ($model->restore()) {
$logaction = new Actionlog();
$logaction->item_type = User::class;
$logaction->item_type = AssetModel::class;
$logaction->item_id = $model->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = auth()->id();

View file

@ -165,7 +165,7 @@ class AssetsController extends Controller
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
@ -388,7 +388,7 @@ class AssetsController extends Controller
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
@ -478,9 +478,16 @@ class AssetsController extends Controller
$tag = $tag ? $tag : $request->get('assetTag');
$topsearch = ($request->get('topsearch') == 'true');
if (! $asset = Asset::where('asset_tag', '=', $tag)->first()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
// Search for an exact and unique asset tag match
$assets = Asset::where('asset_tag', '=', $tag);
// If not a unique result, redirect to the index view
if ($assets->count() != 1) {
return redirect()->route('hardware.index')
->with('search', $tag)
->with('warning', trans('admin/hardware/message.does_not_exist_var', [ 'asset_tag' => $tag ]));
}
$asset = $assets->first();
$this->authorize('view', $asset);
return redirect()->route('hardware.show', $asset->id)->with('topsearch', $topsearch);
@ -837,7 +844,7 @@ class AssetsController extends Controller
{
$this->authorize('checkin', Asset::class);
return view('hardware/quickscan-checkin');
return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList());
}
public function audit($id)

View file

@ -227,7 +227,8 @@ class BulkAssetsController extends Controller
* its checkout status.
*/
if (($request->filled('purchase_date'))
if (($request->filled('name'))
|| ($request->filled('purchase_date'))
|| ($request->filled('expected_checkin'))
|| ($request->filled('purchase_cost'))
|| ($request->filled('supplier_id'))
@ -239,6 +240,7 @@ class BulkAssetsController extends Controller
|| ($request->filled('status_id'))
|| ($request->filled('model_id'))
|| ($request->filled('next_audit_date'))
|| ($request->filled('null_name'))
|| ($request->filled('null_purchase_date'))
|| ($request->filled('null_expected_checkin_date'))
|| ($request->filled('null_next_audit_date'))
@ -251,13 +253,14 @@ class BulkAssetsController extends Controller
$this->update_array = [];
/**
* Leave out model_id and status here because we do math on that later. We have to do some extra
* validation and checks on those two.
* Leave out model_id and status here because we do math on that later. We have to do some
* extra validation and checks on those two.
*
* It's tempting to make these match the request check above, but some of these values require
* extra work to make sure the data makes sense.
*/
$this->conditionallyAddItem('purchase_date')
$this->conditionallyAddItem('name')
->conditionallyAddItem('purchase_date')
->conditionallyAddItem('expected_checkin')
->conditionallyAddItem('order_number')
->conditionallyAddItem('requestable')
@ -271,6 +274,11 @@ class BulkAssetsController extends Controller
/**
* Blank out fields that were requested to be blanked out via checkbox
*/
if ($request->input('null_name')=='1') {
$this->update_array['name'] = null;
}
if ($request->input('null_purchase_date')=='1') {
$this->update_array['purchase_date'] = null;
}

View file

@ -508,8 +508,8 @@ class LoginController extends Controller
protected function validator(array $data)
{
return Validator::make($data, [
'username' => 'required',
'password' => 'required',
'username' => 'required|not_array',
'password' => 'required|not_array',
]);
}

View file

@ -87,7 +87,7 @@ class ResetPasswordController extends Controller
'password.not_in' => trans('validation.disallow_same_pwd_as_user_fields'),
];
$request->validate($this->rules(), $request->all(), $this->validationErrorMessages());
$request->validate($this->rules());
Log::debug('Checking if '.$request->input('username').' exists');
// Check to see if the user even exists - we'll treat the response the same to prevent user sniffing

View file

@ -99,12 +99,18 @@ class SamlController extends Controller
{
$saml = $this->saml;
$auth = $saml->getAuth();
$auth->processResponse();
$saml_exception = false;
try {
$auth->processResponse();
} catch (\Exception $e) {
Log::warning("Exception caught in SAML login: " . $e->getMessage());
$saml_exception = true;
}
$errors = $auth->getErrors();
if (! empty($errors)) {
Log::error('There was an error with SAML ACS: '.implode(', ', $errors));
Log::error('Reason: '.$auth->getLastErrorReason());
if (!empty($errors) || $saml_exception) {
Log::warning('There was an error with SAML ACS: ' . implode(', ', $errors));
Log::warning('Reason: ' . $auth->getLastErrorReason());
return redirect()->route('login')->with('error', trans('auth/message.signin.error'));
}
@ -132,12 +138,18 @@ class SamlController extends Controller
{
$auth = $this->saml->getAuth();
$retrieveParametersFromServer = $this->saml->getSetting('retrieveParametersFromServer', false);
$sloUrl = $auth->processSLO(true, null, $retrieveParametersFromServer, null, true);
$saml_exception = false;
try {
$sloUrl = $auth->processSLO(true, null, $retrieveParametersFromServer, null, true);
} catch (\Exception $e) {
Log::warning("Exception caught in SAML single-logout: " . $e->getMessage());
$saml_exception = true;
}
$errors = $auth->getErrors();
if (! empty($errors)) {
Log::error('There was an error with SAML SLS: '.implode(', ', $errors));
Log::error('Reason: '.$auth->getLastErrorReason());
if (!empty($errors) || $saml_exception) {
Log::warning('There was an error with SAML SLS: ' . implode(', ', $errors));
Log::warning('Reason: ' . $auth->getLastErrorReason());
return view('errors.403');
}

View file

@ -20,7 +20,7 @@ trait CheckInOutRequest
return Location::findOrFail(request('assigned_location'));
case 'asset':
return Asset::findOrFail(request('assigned_asset'));
case 'user':
default:
return User::findOrFail(request('assigned_user'));
}

View file

@ -8,6 +8,7 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\Component;
use App\Models\Setting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input;
@ -97,6 +98,10 @@ class ComponentCheckoutController extends Controller
// Check if the asset exists
$asset = Asset::find($request->input('asset_id'));
if ((Setting::getSettings()->full_multiple_companies_support) && $component->company_id !== $asset->company_id) {
return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_user_company'));
}
// Update the component data
$component->asset_id = $request->input('asset_id');
$component->assets()->attach($component->id, [

View file

@ -62,6 +62,20 @@ class DepreciationsController extends Controller
$depreciation->name = $request->input('name');
$depreciation->months = $request->input('months');
$depreciation->user_id = Auth::id();
$request->validate([
'depreciation_min' => [
'required',
'numeric',
function ($attribute, $value, $fail) use ($request) {
if ($request->input('depreciation_type') == 'percent' && ($value < 0 || $value > 100)) {
$fail(trans('validation.percent'));
}
},
],
'depreciation_type' => 'required|in:amount,percent',
]);
$depreciation->depreciation_type = $request->input('depreciation_type');
$depreciation->depreciation_min = $request->input('depreciation_min');
// Was the asset created?
@ -116,6 +130,20 @@ class DepreciationsController extends Controller
// Depreciation data
$depreciation->name = $request->input('name');
$depreciation->months = $request->input('months');
$request->validate([
'depreciation_min' => [
'required',
'numeric',
function ($attribute, $value, $fail) use ($request) {
if ($request->input('depreciation_type') == 'percent' && ($value < 0 || $value > 100)) {
$fail(trans('validation.percent'));
}
},
],
'depreciation_type' => 'required|in:amount,percent',
]);
$depreciation->depreciation_type = $request->input('depreciation_type');
$depreciation->depreciation_min = $request->input('depreciation_min');
// Was the asset created?
@ -165,13 +193,20 @@ class DepreciationsController extends Controller
*/
public function show($id) : View | RedirectResponse
{
if (is_null($depreciation = Depreciation::find($id))) {
// Redirect to the blogs management page
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
}
$depreciation = Depreciation::withCount('assets as assets_count')
->withCount('models as models_count')
->withCount('licenses as licenses_count')
->find($id);
$this->authorize('view', $depreciation);
return view('depreciations/view', compact('depreciation'));
if ($depreciation) {
return view('depreciations/view', compact('depreciation'));
}
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
}
}

View file

@ -62,10 +62,10 @@ class CheckoutKitController extends Controller
$checkout_result = $this->kitService->checkout($request, $kit, $user);
if (Arr::has($checkout_result, 'errors') && count($checkout_result['errors']) > 0) {
return redirect()->back()->with('error', trans('general.checkout_error'))->with('error_messages', $checkout_result['errors']);
return redirect()->back()->with('error', trans('admin/kits/general.checkout_error'))->with('error_messages', $checkout_result['errors']);
}
return redirect()->back()->with('success', trans('general.checkout_success'))
return redirect()->back()->with('success', trans('admin/kits/general.checkout_success'))
->with('assets', Arr::get($checkout_result, 'assets', null))
->with('accessories', Arr::get($checkout_result, 'accessories', null))
->with('consumables', Arr::get($checkout_result, 'consumables', null));

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Location;
use App\Models\User;
@ -193,7 +194,13 @@ class LocationsController extends Controller
*/
public function show($id = null) : View | RedirectResponse
{
$location = Location::find($id);
$location = Location::withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withTrashed()
->find($id);
if (isset($location->id)) {
return view('locations/view', compact('location'));
@ -249,6 +256,41 @@ class LocationsController extends Controller
}
/**
* Restore a given Asset Model (mark as un-deleted)
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $id
*/
public function postRestore($id) : RedirectResponse
{
$this->authorize('create', Location::class);
if ($location = Location::withTrashed()->find($id)) {
if ($location->deleted_at == '') {
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.location')]));
}
if ($location->restore()) {
$logaction = new Actionlog();
$logaction->item_type = Location::class;
$logaction->item_id = $location->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = auth()->id();
$logaction->logaction('restore');
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
{
if ($location = Location::where('id', $id)->first()) {

View file

@ -49,6 +49,8 @@ class ProfileController extends Controller
$user->gravatar = $request->input('gravatar');
$user->skin = $request->input('skin');
$user->phone = $request->input('phone');
$user->enable_sounds = $request->input('enable_sounds', false);
$user->enable_confetti = $request->input('enable_confetti', false);
if (! config('app.lock_passwords')) {
$user->locale = $request->input('locale', 'en-US');

View file

@ -324,6 +324,7 @@ class SettingsController extends Controller
$setting->full_multiple_companies_support = $request->input('full_multiple_companies_support', '0');
$setting->unique_serial = $request->input('unique_serial', '0');
$setting->shortcuts_enabled = $request->input('shortcuts_enabled', '0');
$setting->show_images_in_email = $request->input('show_images_in_email', '0');
$setting->show_archived_in_list = $request->input('show_archived_in_list', '0');
$setting->dashboard_message = $request->input('dashboard_message');
@ -636,6 +637,7 @@ class SettingsController extends Controller
$setting->alert_threshold = $request->input('alert_threshold');
$setting->audit_interval = $request->input('audit_interval');
$setting->audit_warning_days = $request->input('audit_warning_days');
$setting->due_checkin_days = $request->input('due_checkin_days');
$setting->show_alerts_in_menu = $request->input('show_alerts_in_menu', '0');
if ($setting->save()) {
@ -1202,7 +1204,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.0]
*/
public function postRestore($filename = null) : RedirectResponse
public function postRestore(Request $request, $filename = null): RedirectResponse
{
if (! config('app.lock_passwords')) {
@ -1222,13 +1224,29 @@ class SettingsController extends Controller
Log::debug('Attempting to restore from: '. storage_path($path).'/'.$filename);
// run the restore command
Artisan::call('snipeit:restore',
[
$restore_params = [
'--force' => true,
'--no-progress' => true,
'filename' => storage_path($path).'/'.$filename
]);
'filename' => storage_path($path) . '/' . $filename
];
if ($request->input('clean')) {
Log::debug("Attempting 'clean' - first, guessing prefix...");
Artisan::call('snipeit:restore', [
'--sanitize-guess-prefix' => true,
'filename' => storage_path($path) . '/' . $filename
]);
$guess_prefix_output = Artisan::output();
Log::debug("Sanitize output is: $guess_prefix_output");
list($prefix, $_output) = explode("\n", $guess_prefix_output);
Log::debug("prefix is: '$prefix'");
$restore_params['--sanitize-with-prefix'] = $prefix;
}
// run the restore command
Artisan::call('snipeit:restore',
$restore_params
);
// If it's greater than 300, it probably worked
$output = Artisan::output();
@ -1255,7 +1273,7 @@ class SettingsController extends Controller
DB::table('users')->update(['remember_token' => null]);
Auth::logout();
return redirect()->route('login')->with('success', 'Your system has been restored. Please login again.');
return redirect()->route('login')->with('success', trans('admin/settings/message.restore.success'));
} else {
return redirect()->route('settings.backups.index')->with('error', trans('admin/settings/message.backup.file_not_found'));
}

View file

@ -16,6 +16,7 @@ use App\Models\Consumable;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Password;
@ -29,7 +30,7 @@ class BulkUsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.7]
* @param Request $request
* @return \Illuminate\Contracts\View\View
* @return \Illuminate\Contracts\View\View | \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function edit(Request $request)
@ -115,6 +116,8 @@ class BulkUsersController extends Controller
->conditionallyAddItem('remote')
->conditionallyAddItem('ldap_import')
->conditionallyAddItem('activated')
->conditionallyAddItem('start_date')
->conditionallyAddItem('end_date')
->conditionallyAddItem('autoassign_licenses');
@ -145,7 +148,14 @@ class BulkUsersController extends Controller
$this->update_array['company_id'] = null;
}
if ($request->input('null_start_date')=='1') {
$this->update_array['start_date'] = null;
}
if ($request->input('null_end_date')=='1') {
$this->update_array['end_date'] = null;
}
if (! $manager_conflict) {
$this->conditionallyAddItem('manager_id');
}
@ -218,21 +228,19 @@ class BulkUsersController extends Controller
}
$users = User::whereIn('id', $user_raw_array)->get();
$assets = Asset::whereIn('assigned_to', $user_raw_array)->where('assigned_type', \App\Models\User::class)->get();
$accessories = DB::table('accessories_users')->whereIn('assigned_to', $user_raw_array)->get();
$assets = Asset::whereIn('assigned_to', $user_raw_array)->where('assigned_type', User::class)->get();
$accessoryUserRows = DB::table('accessories_checkout')->where('assigned_type', User::class)->whereIn('assigned_to', $user_raw_array)->get();
$licenses = DB::table('license_seats')->whereIn('assigned_to', $user_raw_array)->get();
$consumables = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get();
$consumableUserRows = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get();
if ((($assets->count() > 0) && ((!$request->filled('status_id')) || ($request->input('status_id') == '')))) {
return redirect()->route('users.index')->with('error', 'No status selected');
}
$this->logItemCheckinAndDelete($assets, Asset::class);
$this->logItemCheckinAndDelete($accessories, Accessory::class);
$this->logAccessoriesCheckin($accessoryUserRows);
$this->logItemCheckinAndDelete($licenses, License::class);
$this->logItemCheckinAndDelete($consumables, Consumable::class);
$this->logConsumablesCheckin($consumableUserRows);
Asset::whereIn('id', $assets->pluck('id'))->update([
'status_id' => e(request('status_id')),
@ -241,19 +249,14 @@ class BulkUsersController extends Controller
'expected_checkin' => null,
]);
LicenseSeat::whereIn('id', $licenses->pluck('id'))->update(['assigned_to' => null]);
ConsumableAssignment::whereIn('id', $consumables->pluck('id'))->delete();
ConsumableAssignment::whereIn('id', $consumableUserRows->pluck('id'))->delete();
foreach ($users as $user) {
$user->consumables()->sync([]);
$user->accessories()->sync([]);
if ($request->input('delete_user')=='1') {
$user->delete();
}
}
$msg = trans('general.bulk_checkin_success');
@ -279,7 +282,7 @@ class BulkUsersController extends Controller
if ($itemType == License::class){
$item_id = $item->license_id;
}
$logAction->item_id = $item_id;
// We can't rely on get_class here because the licenses/accessories fetched above are not eloquent models, but simply arrays.
$logAction->item_type = $itemType;
@ -291,6 +294,34 @@ class BulkUsersController extends Controller
}
}
private function logAccessoriesCheckin(Collection $accessoryUserRows): void
{
foreach ($accessoryUserRows as $accessoryUserRow) {
$logAction = new Actionlog();
$logAction->item_id = $accessoryUserRow->accessory_id;
$logAction->item_type = Accessory::class;
$logAction->target_id = $accessoryUserRow->assigned_to;
$logAction->target_type = User::class;
$logAction->user_id = Auth::id();
$logAction->note = 'Bulk checkin items';
$logAction->logaction('checkin from');
}
}
private function logConsumablesCheckin(Collection $consumableUserRows): void
{
foreach ($consumableUserRows as $consumableUserRow) {
$logAction = new Actionlog();
$logAction->item_id = $consumableUserRow->consumable_id;
$logAction->item_type = Consumable::class;
$logAction->target_id = $consumableUserRow->assigned_to;
$logAction->target_type = User::class;
$logAction->user_id = Auth::id();
$logAction->note = 'Bulk checkin items';
$logAction->logaction('checkin from');
}
}
/**
* Save bulk-edited users
*

View file

@ -186,7 +186,7 @@ class UsersController extends Controller
{
$this->authorize('update', User::class);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id);
$user = User::with(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed()->find($id);
if ($user) {
@ -214,83 +214,79 @@ class UsersController extends Controller
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(SaveUserRequest $request, $id = null)
public function update(SaveUserRequest $request, User $user)
{
$this->authorize('update', User::class);
// This is a janky hack to prevent people from changing admin demo user data on the public demo.
// The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder.
// Thanks, jerks. You are why we can't have nice things. - snipe
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
if ((($user->id == 1) || ($user->id == 2)) && (config('app.lock_passwords'))) {
return redirect()->route('users.index')->with('error', trans('general.permission_denied_superuser_demo'));
}
// We need to reverse the UI specific logic for our
// permissions here before we update the user.
$permissions = $request->input('permissions', []);
app('request')->request->set('permissions', $permissions);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id);
$user->load(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed();
// User is valid - continue...
if ($user) {
$this->authorize('update', $user);
$this->authorize('update', $user);
// Figure out of this user was an admin before this edit
$orig_permissions_array = $user->decodePermissions();
$orig_superuser = '0';
if (is_array($orig_permissions_array)) {
if (array_key_exists('superuser', $orig_permissions_array)) {
$orig_superuser = $orig_permissions_array['superuser'];
}
// Figure out of this user was an admin before this edit
$orig_permissions_array = $user->decodePermissions();
$orig_superuser = '0';
if (is_array($orig_permissions_array)) {
if (array_key_exists('superuser', $orig_permissions_array)) {
$orig_superuser = $orig_permissions_array['superuser'];
}
}
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
// Update the user fields
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->location_id = $request->input('location_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
$user->notes = $request->input('notes');
$user->department_id = $request->input('department_id', null);
$user->address = $request->input('address', null);
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
// if a user is editing themselves we should always keep activated true
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
$user->website = $request->input('website', null);
$user->start_date = $request->input('start_date', null);
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
// Update the user fields
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->location_id = $request->input('location_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
$user->notes = $request->input('notes');
$user->department_id = $request->input('department_id', null);
$user->address = $request->input('address', null);
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
// if a user is editing themselves we should always keep activated true
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
$user->website = $request->input('website', null);
$user->start_date = $request->input('start_date', null);
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]);
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
// Update the location of any assets checked out to this user
@ -318,13 +314,7 @@ class UsersController extends Controller
return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users'))
->with('success', trans('admin/users/message.success.update'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
/**
@ -601,29 +591,29 @@ class UsersController extends Controller
/**
* Print inventory
*
* @author Aladin Alaily
* @since [v1.8]
* @return \Illuminate\Http\RedirectResponse
* @author Aladin Alaily
*/
public function printInventory($id)
{
$this->authorize('view', User::class);
$user = User::where('id', $id)->withTrashed()->first();
if ($user = User::where('id', $id)->withTrashed()->first()) {
// Make sure they can view this particular user
$this->authorize('view', $user);
$this->authorize('view', $user);
$assets = Asset::where('assigned_to', $id)->where('assigned_type', User::class)->with('model', 'model.category')->get();
$accessories = $user->accessories()->get();
$consumables = $user->consumables()->get();
$assets = Asset::where('assigned_to', $id)->where('assigned_type', User::class)->with('model', 'model.category')->get();
$accessories = $user->accessories()->get();
$consumables = $user->consumables()->get();
return view('users/print')->with('assets', $assets)
->with('licenses', $user->licenses()->get())
->with('accessories', $accessories)
->with('consumables', $consumables)
->with('show_user', $user)
->with('settings', Setting::getSettings());
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
return view('users/print')->with('assets', $assets)
->with('licenses', $user->licenses()->get())
->with('accessories', $accessories)
->with('consumables', $consumables)
->with('show_user', $user)
->with('settings', Setting::getSettings());
}
/**

View file

@ -14,6 +14,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\NoSessionStore::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Session\Middleware\StartSession::class,
@ -21,6 +22,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\CheckForSetup::class,
\App\Http\Middleware\CheckForDebug::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\SecurityHeaders::class,
\App\Http\Middleware\PreventBackHistory::class,
\Illuminate\Http\Middleware\HandleCors::class,
@ -48,6 +50,7 @@ class Kernel extends HttpKernel
'api' => [
'auth:api',
\App\Http\Middleware\CheckLocale::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

View file

@ -44,13 +44,10 @@ class AccessoryCheckoutRequest extends ImageUploadRequest
return array_merge(
[
'assigned_to' => [
'required',
'integer',
'exists:users,id,deleted_at,NULL',
'not_array'
],
'assigned_user' => 'required_without_all:assigned_asset,assigned_location',
'assigned_asset' => 'required_without_all:assigned_user,assigned_location',
'assigned_location' => 'required_without_all:assigned_user,assigned_asset',
'number_remaining_after_checkout' => [
'min:0',
'required',

View file

@ -6,6 +6,7 @@ use App\Models\Setting;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Rules\UserCannotSwitchCompaniesIfItemsAssigned;
class SaveUserRequest extends FormRequest
{
@ -34,6 +35,7 @@ class SaveUserRequest extends FormRequest
$rules = [
'department_id' => 'nullable|exists:departments,id',
'manager_id' => 'nullable|exists:users,id',
'company_id' => ['nullable','exists:companies,id']
];
switch ($this->method()) {
@ -52,11 +54,13 @@ class SaveUserRequest extends FormRequest
$rules['first_name'] = 'required|string|min:1';
$rules['username'] = 'required_unless:ldap_import,1|string|min:1';
$rules['password'] = Setting::passwordComplexityRulesSaving('update').'|confirmed';
$rules['company_id'] = [new UserCannotSwitchCompaniesIfItemsAssigned()];
break;
// Save only what's passed
case 'PATCH':
$rules['password'] = Setting::passwordComplexityRulesSaving('update');
$rules['company_id'] = [new UserCannotSwitchCompaniesIfItemsAssigned()];
break;
default:

View file

@ -2,6 +2,7 @@
namespace App\Http\Requests;
use App\Http\Requests\Traits\MayContainCustomFields;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Setting;
@ -11,6 +12,7 @@ use Illuminate\Support\Facades\Gate;
class StoreAssetRequest extends ImageUploadRequest
{
use MayContainCustomFields;
/**
* Determine if the user is authorized to make this request.
*

View file

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\Traits;
use App\Models\AssetModel;
use App\Models\CustomField;
trait MayContainCustomFields
{
// this gets called automatically on a form request
public function withValidator($validator)
{
// find the model
if ($this->method() == 'POST') {
$asset_model = AssetModel::find($this->model_id);
}
if ($this->method() == 'PATCH' || $this->method() == 'PUT') {
$asset_model = $this->asset->model;
}
// collect the custom fields in the request
$validator->after(function ($validator) use ($asset_model) {
$request_fields = $this->collect()->keys()->filter(function ($attributes) {
return str_starts_with($attributes, '_snipeit_');
});
// if there are custom fields, find the one's that don't exist on the model's fieldset and add an error to the validator's error bag
if (count($request_fields) > 0) {
$request_fields->diff($asset_model?->fieldset?->fields?->pluck('db_column'))
->each(function ($request_field_name) use ($request_fields, $validator) {
if (CustomField::where('db_column', $request_field_name)->exists()) {
$validator->errors()->add($request_field_name, trans('validation.custom.custom_field_not_found_on_model'));
} else {
$validator->errors()->add($request_field_name, trans('validation.custom.custom_field_not_found'));
}
});
}
});
}
}

View file

@ -2,12 +2,15 @@
namespace App\Http\Requests;
use App\Http\Requests\Traits\MayContainCustomFields;
use App\Models\Asset;
use App\Models\Setting;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
class UpdateAssetRequest extends ImageUploadRequest
{
use MayContainCustomFields;
/**
* Determine if the user is authorized to make this request.
*
@ -39,6 +42,12 @@ class UpdateAssetRequest extends ImageUploadRequest
],
);
// if the purchase cost is passed in as a string **and** the digit_separator is ',' (as is common in the EU)
// then we tweak the purchase_cost rule to make it a string
if (Setting::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
$rules['purchase_cost'] = ['nullable', 'string'];
}
return $rules;
}
}

View file

@ -11,15 +11,17 @@ trait TwoColumnUniqueUndeletedTrait
* @param string $field
* @return string
*/
protected function prepareTwoColumnUniqueUndeletedRule($parameters, $field)
protected function prepareTwoColumnUniqueUndeletedRule($parameters)
{
$column = $parameters[0];
$value = $this->{$parameters[0]};
// This is an existing model we're updating so ignore the current ID ($this->getKey())
if ($this->exists) {
return 'two_column_unique_undeleted:'.$this->table.','.$this->getKey().','.$column.','.$value;
}
// This is a new record, so we can ignore the current ID
return 'two_column_unique_undeleted:'.$this->table.',0,'.$column.','.$value;
}
}

View file

@ -39,7 +39,7 @@ class AccessoriesTransformer
'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null,
'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,
'remaining_qty' => (int) $accessory->numRemaining(),
'users_count' => $accessory->users_count,
'checkouts_count' => $accessory->checkouts_count,
'created_at' => Helper::getFormattedDateObject($accessory->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($accessory->updated_at, 'datetime'),
@ -66,27 +66,42 @@ class AccessoriesTransformer
return $array;
}
public function transformCheckedoutAccessory($accessory, $accessory_users, $total)
public function transformCheckedoutAccessory($accessory, $accessory_checkouts, $total)
{
$array = [];
foreach ($accessory_users as $user) {
foreach ($accessory_checkouts as $checkout) {
$array[] = [
'assigned_pivot_id' => $user->pivot->id,
'id' => (int) $user->id,
'username' => e($user->username),
'name' => e($user->getFullNameAttribute()),
'first_name'=> e($user->first_name),
'last_name'=> e($user->last_name),
'employee_number' => e($user->employee_num),
'checkout_notes' => e($user->pivot->note),
'last_checkout' => Helper::getFormattedDateObject($user->pivot->created_at, 'datetime'),
'type' => 'user',
'id' => $checkout->id,
'assigned_to' => $this->transformAssignedTo($checkout),
'checkout_notes' => e($checkout->note),
'last_checkout' => Helper::getFormattedDateObject($checkout->created_at, 'datetime'),
'available_actions' => ['checkin' => true],
];
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformAssignedTo($accessoryCheckout)
{
if ($accessoryCheckout->checkedOutToUser()) {
return [
'id' => (int) $accessoryCheckout->assigned->id,
'username' => e($accessoryCheckout->assigned->username),
'name' => e($accessoryCheckout->assigned->getFullNameAttribute()),
'first_name'=> e($accessoryCheckout->assigned->first_name),
'last_name'=> ($accessoryCheckout->assigned->last_name) ? e($accessoryCheckout->assigned->last_name) : null,
'email'=> ($accessoryCheckout->assigned->email) ? e($accessoryCheckout->assigned->email) : null,
'employee_number' => ($accessoryCheckout->assigned->employee_num) ? e($accessoryCheckout->assigned->employee_num) : null,
'type' => 'user',
];
}
return $accessoryCheckout->assigned ? [
'id' => $accessoryCheckout->assigned->id,
'name' => e($accessoryCheckout->assigned->display_name),
'type' => $accessoryCheckout->assignedType(),
] : null;
}
}

View file

@ -205,11 +205,11 @@ class ActionlogsTransformer
public function transformCheckedoutActionlog (Collection $accessories_users, $total)
public function transformCheckedoutActionlog (Collection $accessories_checkout, $total)
{
$array = array();
foreach ($accessories_users as $user) {
foreach ($accessories_checkout as $user) {
$array[] = (new UsersTransformer)->transformUser($user);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);

View file

@ -86,7 +86,7 @@ class AssetsTransformer
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date, 'date'),
'deleted_at' => Helper::getFormattedDateObject($asset->deleted_at, 'datetime'),
'purchase_date' => Helper::getFormattedDateObject($asset->purchase_date, 'date'),
'age' => $asset->purchase_date ? $asset->purchase_date->diffForHumans() : '',
'age' => $asset->purchase_date ? $asset->purchase_date->locale(app()->getLocale())->diffForHumans() : '',
'last_checkout' => Helper::getFormattedDateObject($asset->last_checkout, 'datetime'),
'last_checkin' => Helper::getFormattedDateObject($asset->last_checkin, 'datetime'),
'expected_checkin' => Helper::getFormattedDateObject($asset->expected_checkin, 'date'),

View file

@ -7,6 +7,7 @@ use App\Models\Depreciable;
use App\Models\Depreciation;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Log;
class DepreciationsTransformer
{
@ -26,7 +27,10 @@ class DepreciationsTransformer
'id' => (int) $depreciation->id,
'name' => e($depreciation->name),
'months' => $depreciation->months.' '.trans('general.months'),
'depreciation_min' => $depreciation->depreciation_min,
'depreciation_min' => $depreciation->depreciation_type === 'percent' ? $depreciation->depreciation_min.'%' : $depreciation->depreciation_min,
'assets_count' => $depreciation->assets_count,
'models_count' => $depreciation->models_count,
'licenses_count' => $depreciation->licenses_count,
'created_at' => Helper::getFormattedDateObject($depreciation->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($depreciation->updated_at, 'datetime')
];

View file

@ -71,8 +71,10 @@ class AssetImporter extends ItemImporter
$asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
if ($asset) {
if (! $this->updating) {
$this->log('A matching Asset '.$asset_tag.' already exists');
return;
$exists_error = trans('general.import_asset_tag_exists', ['asset_tag' => $asset_tag]);
$this->log($exists_error);
$this->addErrorToBag($asset, 'asset_tag', $exists_error);
return $exists_error;
}
$this->log('Updating Asset');

View file

@ -281,6 +281,13 @@ abstract class Importer
}
}
protected function addErrorToBag($item, $field, $error_message)
{
if ($this->errorCallback) {
call_user_func($this->errorCallback, $item, $field, [$field => [$error_message]]);
}
}
/**
* Finds the user matching given data, or creates a new one if there is no match.
* This is NOT used by the User Import, only for Asset/Accessory/etc where

View file

@ -196,64 +196,77 @@ class ItemImporter extends Importer
{
$condition = array();
$asset_model_name = $this->findCsvMatch($row, 'asset_model');
$asset_model_category = $this->findCsvMatch($row, 'category');
$asset_modelNumber = $this->findCsvMatch($row, 'model_number');
// TODO: At the moment, this means we can't update the model number if the model name stays the same.
if (! $this->shouldUpdateField($asset_model_name)) {
return;
}
if ((empty($asset_model_name)) && (! empty($asset_modelNumber))) {
$asset_model_name = $asset_modelNumber;
} elseif ((empty($asset_model_name)) && (empty($asset_modelNumber))) {
$asset_model_name = 'Unknown';
}
if ((!empty($asset_model_name)) && (empty($asset_modelNumber))) {
$condition[] = ['name', '=', $asset_model_name];
} elseif ((!empty($asset_model_name)) && (!empty($asset_modelNumber))) {
$condition[] = ['name', '=', $asset_model_name];
$condition[] = ['model_number', '=', $asset_modelNumber];
$asset_model = AssetModel::select('id');
if (!empty($asset_model_name)) {
$asset_model = $asset_model->where('name', '=', $asset_model_name);
if (!empty($asset_modelNumber)) {
$asset_model = $asset_model->where('model_number', '=', $asset_modelNumber);
}
}
$editingModel = $this->updating;
$asset_model = AssetModel::where($condition)->first();
$asset_model = $asset_model->first();
if ($asset_model) {
if (! $this->updating) {
$this->log('A matching model already exists, returning it.');
return $asset_model->id;
}
$this->log('Matching Model found, updating it.');
$item = $this->sanitizeItemForStoring($asset_model, $editingModel);
$item['name'] = $asset_model_name;
$item['notes'] = $this->findCsvMatch($row, 'model_notes');
if(!empty($asset_modelNumber)){
if (!empty($asset_modelNumber)){
$item['model_number'] = $asset_modelNumber;
}
$asset_model->update($item);
$asset_model->save();
$this->log('Asset Model Updated');
return $asset_model->id;
}
$this->log('No Matching Model, Creating a new one');
}
$this->log('No Matching Model, Creating a new one');
$asset_model = new AssetModel();
$item = $this->sanitizeItemForStoring($asset_model, $editingModel);
$item['name'] = $asset_model_name;
$item['model_number'] = $asset_modelNumber;
$item['notes'] = $this->findCsvMatch($row, 'model_notes');
$item['category_id'] = $this->createOrFetchCategory($asset_model_category);
$asset_model->fill($item);
//$asset_model = AssetModel::firstOrNew($item);
$item = null;
if ($asset_model->save()) {
$this->log('Asset Model '.$asset_model_name.' with model number '.$asset_modelNumber.' was created');
return $asset_model->id;
}
$this->log('Asset Model Errors: '.$asset_model->getErrors());
$this->logError($asset_model, 'Asset Model "'.$asset_model_name.'"');
return null;

View file

@ -137,7 +137,11 @@ class CheckoutableListener
*/
private function getCheckoutAcceptance($event)
{
if (! $event->checkoutable->requireAcceptance()) {
$checkedOutToType = get_class($event->checkedOutTo);
if ($checkedOutToType != "App\Models\User") {
return null;
}
if (!$event->checkoutable->requireAcceptance()) {
return null;
}

View file

@ -3,30 +3,25 @@
namespace App\Livewire;
use App\Models\CustomField;
use Livewire\Component;
use App\Models\Import;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Computed;
use Livewire\Component;
class Importer extends Component
{
use AuthorizesRequests;
public $files;
public $progress; //upload progress - '-1' means don't show
public $progress = -1; //upload progress - '-1' means don't show
public $progress_message;
public $progress_bar_class;
public $progress_bar_class = 'progress-bar-warning';
public $message; //status/error message?
public $message_type; //success/error?
//originally from ImporterFile
public $import_errors; //
public ?Import $activeFile = null;
public $activeFileId;
public $headerRow = [];
public $typeOfImport;
public $importTypes;
public $columnOptions;
public $statusType;
@ -35,7 +30,6 @@ class Importer extends Component
public $send_welcome;
public $run_backup;
public $field_map; // we need a separate variable for the field-mapping, because the keys in the normal array are too complicated for Livewire to understand
public $file_id; // TODO: I can't figure out *why* we need this, but it really seems like we do. I can't seem to pull the id from the activeFile for some reason?
// Make these variables public - we set the properties in the constructor so we can localize them (versus the old static arrays)
public $accessories_fields;
@ -51,10 +45,8 @@ class Importer extends Component
'files.*.file_path' => 'required|string',
'files.*.created_at' => 'required|string',
'files.*.filesize' => 'required|integer',
'activeFile' => 'Import',
'activeFile.import_type' => 'string',
'activeFile.field_map' => 'array',
'activeFile.header_row' => 'array',
'headerRow' => 'array',
'typeOfImport' => 'string',
'field_map' => 'array'
];
@ -68,15 +60,13 @@ class Importer extends Component
{
$tmp = array();
if ($this->activeFile) {
$tmp = array_combine($this->activeFile->header_row, $this->field_map);
$tmp = array_combine($this->headerRow, $this->field_map);
$tmp = array_filter($tmp);
}
return json_encode($tmp);
}
private function getColumns($type)
{
switch ($type) {
@ -115,76 +105,66 @@ class Importer extends Component
return $results;
}
public function updating($name, $new_import_type)
public function updatingTypeOfImport($type)
{
if ($name == "activeFile.import_type") {
// go through each header, find a matching field to try and map it to.
foreach ($this->activeFile->header_row as $i => $header) {
// do we have something mapped already?
if (array_key_exists($i, $this->field_map)) {
// yes, we do. Is it valid for this type of import?
// (e.g. the import type might have been changed...?)
if (array_key_exists($this->field_map[$i], $this->columnOptions[$new_import_type])) {
//yes, this key *is* valid. Continue on to the next field.
continue;
} else {
//no, this key is *INVALID* for this import type. Better set it to null
// and we'll hope that the $aliases_fields or something else picks it up.
$this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh.
} // TODO - strictly speaking, this isn't necessary here I don't think.
// go through each header, find a matching field to try and map it to.
foreach ($this->headerRow as $i => $header) {
// do we have something mapped already?
if (array_key_exists($i, $this->field_map)) {
// yes, we do. Is it valid for this type of import?
// (e.g. the import type might have been changed...?)
if (array_key_exists($this->field_map[$i], $this->columnOptions[$type])) {
//yes, this key *is* valid. Continue on to the next field.
continue;
} else {
//no, this key is *INVALID* for this import type. Better set it to null
// and we'll hope that the $aliases_fields or something else picks it up.
$this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh.
} // TODO - strictly speaking, this isn't necessary here I don't think.
}
// first, check for exact matches
foreach ($this->columnOptions[$type] as $v => $text) {
if (strcasecmp($text, $header) === 0) { // case-INSENSITIVe on purpose!
$this->field_map[$i] = $v;
continue 2; //don't bother with the alias check, go to the next header
}
// first, check for exact matches
foreach ($this->columnOptions[$new_import_type] as $value => $text) {
if (strcasecmp($text, $header) === 0) { // case-INSENSITIVe on purpose!
$this->field_map[$i] = $value;
continue 2; //don't bother with the alias check, go to the next header
}
}
// if you got here, we didn't find a match. Try the $aliases_fields
foreach ($this->aliases_fields as $key => $alias_values) {
foreach ($alias_values as $alias_value) {
if (strcasecmp($alias_value, $header) === 0) { // aLsO CaSe-INSENSitiVE!
// Make *absolutely* sure that this key actually _exists_ in this import type -
// you can trigger this by importing accessories with a 'Warranty' column (which don't exist
// in "Accessories"!)
if (array_key_exists($key, $this->columnOptions[$new_import_type])) {
$this->field_map[$i] = $key;
continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header
}
}
// if you got here, we didn't find a match. Try the $aliases_fields
foreach ($this->aliases_fields as $key => $alias_values) {
foreach ($alias_values as $alias_value) {
if (strcasecmp($alias_value, $header) === 0) { // aLsO CaSe-INSENSitiVE!
// Make *absolutely* sure that this key actually _exists_ in this import type -
// you can trigger this by importing accessories with a 'Warranty' column (which don't exist
// in "Accessories"!)
if (array_key_exists($key, $this->columnOptions[$type])) {
$this->field_map[$i] = $key;
continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header
}
}
}
// and if you got here, we got nothing. Let's recommend 'null'
$this->field_map[$i] = null; // Booooo :(
}
// and if you got here, we got nothing. Let's recommend 'null'
$this->field_map[$i] = null; // Booooo :(
}
}
public function boot() { // FIXME - delete or undelete.
///////$this->activeFile = null; // I do *not* understand why I have to do this, but, well, whatever.
}
public function mount()
{
$this->authorize('import');
$this->progress = -1; // '-1' means 'don't show the progressbar'
$this->progress_bar_class = 'progress-bar-warning';
$this->importTypes = [
'asset' => trans('general.assets'),
'accessory' => trans('general.accessories'),
'asset' => trans('general.assets'),
'accessory' => trans('general.accessories'),
'consumable' => trans('general.consumables'),
'component' => trans('general.components'),
'license' => trans('general.licenses'),
'user' => trans('general.users'),
'location' => trans('general.locations'),
'component' => trans('general.components'),
'license' => trans('general.licenses'),
'user' => trans('general.users'),
'location' => trans('general.locations'),
];
/**
* These are the item-type specific columns
*/
$this->accessories_fields = [
$this->accessories_fields = [
'company' => trans('general.company'),
'location' => trans('general.location'),
'quantity' => trans('general.qty'),
@ -307,7 +287,7 @@ class Importer extends Component
'manufacturer' => trans('general.manufacturer'),
];
$this->users_fields = [
$this->users_fields = [
'id' => trans('general.id'),
'company' => trans('general.company'),
'location' => trans('general.location'),
@ -332,12 +312,12 @@ class Importer extends Component
'website' => trans('general.website'),
'avatar' => trans('general.image'),
'gravatar' => trans('general.importer.gravatar'),
'start_date' => trans('general.start_date'),
'end_date' => trans('general.end_date'),
'employee_num' => trans('general.employee_number'),
'start_date' => trans('general.start_date'),
'end_date' => trans('general.end_date'),
'employee_num' => trans('general.employee_number'),
];
$this->locations_fields = [
$this->locations_fields = [
'name' => trans('general.item_name_var', ['item' => trans('general.location')]),
'address' => trans('general.address'),
'address2' => trans('general.importer.address2'),
@ -374,6 +354,12 @@ class Importer extends Component
'model name',
'model',
],
'eol_date' =>
[
'eol',
'eol date',
'asset eol date',
],
'gravatar' =>
[
'gravatar',
@ -504,19 +490,16 @@ class Importer extends Component
];
$this->columnOptions[''] = $this->getColumns(''); //blank mode? I don't know what this is supposed to mean
foreach($this->importTypes AS $type => $name) {
foreach ($this->importTypes as $type => $name) {
$this->columnOptions[$type] = $this->getColumns($type);
}
if ($this->activeFile) {
$this->field_map = $this->activeFile->field_map ? array_values($this->activeFile->field_map) : [];
}
}
public function selectFile($id)
{
$this->clearMessage();
$this->activeFile = Import::find($id);
$this->activeFileId = $id;
if (!$this->activeFile) {
$this->message = trans('admin/hardware/message.import.file_missing');
@ -525,15 +508,17 @@ class Importer extends Component
return;
}
$this->headerRow = $this->activeFile->header_row;
$this->typeOfImport = $this->activeFile->import_type;
$this->field_map = null;
foreach($this->activeFile->header_row as $element) {
if(isset($this->activeFile->field_map[$element])) {
foreach ($this->headerRow as $element) {
if (isset($this->activeFile->field_map[$element])) {
$this->field_map[] = $this->activeFile->field_map[$element];
} else {
$this->field_map[] = null; // re-inject the 'nulls' if a file was imported with some 'Do Not Import' settings
}
}
$this->file_id = $id;
$this->import_errors = null;
$this->statusText = null;
@ -541,21 +526,33 @@ class Importer extends Component
public function destroy($id)
{
// TODO: why don't we just do File::find($id)? This seems dumb.
foreach($this->files as $file) {
if ($id == $file->id) {
if (Storage::delete('private_uploads/imports/'.$file->file_path)) {
$file->delete();
$this->authorize('import');
$this->message = trans('admin/hardware/message.import.file_delete_success');
$this->message_type = 'success';
return;
} else {
$this->message = trans('admin/hardware/message.import.file_delete_error');
$this->message_type = 'danger';
}
}
$import = Import::find($id);
// Check that the import wasn't deleted after while page was already loaded...
// @todo: next up...handle the file being missing for other interactions...
// for example having an import open in two tabs, deleting it, and then changing
// the import type in the other tab. The error message below wouldn't display in that case.
if (!$import) {
$this->message = trans('admin/hardware/message.import.file_already_deleted');
$this->message_type = 'danger';
return;
}
if (Storage::delete('private_uploads/imports/' . $import->file_path)) {
$import->delete();
$this->message = trans('admin/hardware/message.import.file_delete_success');
$this->message_type = 'success';
unset($this->files);
return;
}
$this->message = trans('admin/hardware/message.import.file_delete_error');
$this->message_type = 'danger';
}
public function clearMessage()
@ -564,11 +561,22 @@ class Importer extends Component
$this->message_type = null;
}
#[Computed]
public function files()
{
return Import::orderBy('id', 'desc')->get();
}
#[Computed]
public function activeFile()
{
return Import::find($this->activeFileId);
}
public function render()
{
$this->files = Import::orderBy('id','desc')->get(); //HACK - slows down renders.
return view('livewire.importer')
->extends('layouts.default')
->section('content');
->extends('layouts.default')
->section('content');
}
}

View file

@ -253,9 +253,10 @@ class Accessory extends SnipeModel
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function users()
public function checkouts()
{
return $this->belongsToMany(\App\Models\User::class, 'accessories_users', 'accessory_id', 'assigned_to')->withPivot('id', 'created_at', 'note')->withTrashed();
return $this->hasMany(\App\Models\AccessoryCheckout::class, 'accessory_id')
->with('assignedTo');
}
/**
@ -267,7 +268,9 @@ class Accessory extends SnipeModel
*/
public function hasUsers()
{
return $this->belongsToMany(\App\Models\User::class, 'accessories_users', 'accessory_id', 'assigned_to')->count();
return $this->hasMany(\App\Models\AccessoryCheckout::class, 'accessory_id')
->where('assigned_type', User::class)
->count();
}
/**
@ -338,15 +341,15 @@ class Accessory extends SnipeModel
*/
public function numCheckedOut()
{
return $this->users_count ?? $this->users()->count();
return $this->checkouts_count ?? $this->checkouts()->count();
}
/**
* Check how many items of an accessory remain.
*
* In order to use this model method, you MUST call withCount('users as users_count')
* on the eloquent query in the controller, otherwise $this->users_count will be null and
* In order to use this model method, you MUST call withCount('checkouts as checkouts_count')
* on the eloquent query in the controller, otherwise $this->checkouts_count will be null and
* bad things happen.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
@ -370,12 +373,12 @@ class Accessory extends SnipeModel
*/
public function declinedCheckout(User $declinedBy, $signature)
{
if (is_null($accessory_user = \DB::table('accessories_users')->where('assigned_to', $declinedBy->id)->where('accessory_id', $this->id)->latest('created_at'))) {
if (is_null($accessory_checkout = AccessoryCheckout::userAssigned()->where('assigned_to', $declinedBy->id)->where('accessory_id', $this->id)->latest('created_at'))) {
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
$accessory_user->limit(1)->delete();
$accessory_checkout->limit(1)->delete();
}
/**

148
app/Models/AccessoryCheckout.php Executable file
View file

@ -0,0 +1,148 @@
<?php
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\Acceptable;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
/**
* Model for Accessories.
*
* @version v1.0
*/
class AccessoryCheckout extends Model
{
use Searchable;
protected $fillable = ['user_id', 'accessory_id', 'assigned_to', 'assigned_type', 'note'];
protected $table = 'accessories_checkout';
/**
* Establishes the accessory checkout -> accessory relationship
*
* @author [A. Kroeger]
* @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessory()
{
return $this->hasOne(\App\Models\Accessory::class, 'accessory_id');
}
/**
* Establishes the accessory checkout -> user relationship
*
* @author [A. Kroeger]
* @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
{
return $this->hasOne(\App\Models\User::class, 'user_id');
}
/**
* Get the target this asset is checked out to
*
* @author [A. Kroeger]
* @since [v7.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assignedTo()
{
return $this->morphTo('assigned', 'assigned_type', 'assigned_to')->withTrashed();
}
/**
* Gets the lowercased name of the type of target the asset is assigned to
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return string
*/
public function assignedType()
{
return $this->assigned_type ? strtolower(class_basename($this->assigned_type)) : null;
}
/**
* Determines whether the accessory is checked out to a user
*
* Even though we allow allow for checkout to things beyond users
* this method is an easy way of seeing if we are checked out to a user.
*
* @author [A. Kroeger]
* @since [v7.0]
*/
public function checkedOutToUser(): bool
{
return $this->assignedType() === Asset::USER;
}
public function scopeUserAssigned(Builder $query): void
{
$query->where('assigned_type', '=', User::class);
}
/**
* Run additional, advanced searches.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $terms The search terms
* @return \Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, array $terms)
{
$userQuery = User::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
}
})->select('id');
$locationQuery = Location::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
})->select('id');
$assetQuery = Asset::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
})->select('id');
$query->where(function ($query) use ($userQuery) {
$query->where('assigned_type', User::class)
->whereIn('assigned_to', $userQuery);
})->orWhere(function($query) use ($locationQuery) {
$query->where('assigned_type', Location::class)
->whereIn('assigned_to', $locationQuery);
})->orWhere(function($query) use ($assetQuery) {
$query->where('assigned_type', Asset::class)
->whereIn('assigned_to', $assetQuery);
})->orWhere(function($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('note', 'like', $search_str);
}
});
return $query;
}
}

View file

@ -7,7 +7,6 @@ use App\Presenters\Presentable;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
/**
* Model for the Actionlog (the table that keeps a historical log of
@ -17,10 +16,12 @@ use Illuminate\Support\Facades\Auth;
*/
class Actionlog extends SnipeModel
{
use CompanyableTrait;
use HasFactory;
// This is to manually set the source (via setActionSource()) for determineActionSource()
protected ?string $source = null;
protected $with = ['admin'];
protected $presenter = \App\Presenters\ActionlogPresenter::class;
use SoftDeletes;
@ -372,4 +373,9 @@ class Actionlog extends SnipeModel
{
$this->source = $source;
}
public function scopeOrderAdmin($query, $order)
{
return $query->leftJoin('users as admin_sort', 'action_logs.user_id', '=', 'admin_sort.id')->select('action_logs.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order);
}
}

View file

@ -2,7 +2,6 @@
namespace App\Models;
use App\Events\AssetCheckedOut;
use App\Events\CheckoutableCheckedOut;
use App\Exceptions\CheckoutNotAllowed;
use App\Helpers\Helper;
@ -31,6 +30,7 @@ class Asset extends Depreciable
{
protected $presenter = AssetPresenter::class;
protected $with = ['model', 'admin'];
use CompanyableTrait;
use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait;
@ -716,7 +716,7 @@ class Asset extends Depreciable
* @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
public function admin()
{
return $this->belongsTo(\App\Models\User::class, 'user_id');
}
@ -1315,7 +1315,7 @@ class Asset extends Depreciable
public function scopeDueForCheckin($query, $settings)
{
$interval = $settings->audit_warning_days ?? 0;
$interval = $settings->due_checkin_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval)->format('Y-m-d');
@ -1561,7 +1561,7 @@ class Asset extends Depreciable
$leftJoin->on('assets_dept_users.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', User::class);
})->where(function ($query) use ($search) {
$query->where('assets_dept_users.department_id', '=', $search);
$query->whereIn('assets_dept_users.department_id', $search);
})->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug
}
@ -1811,7 +1811,9 @@ class Asset extends Depreciable
public function scopeInCategory($query, $category_id)
{
return $query->join('models as category_models', 'assets.model_id', '=', 'category_models.id')
->join('categories', 'category_models.category_id', '=', 'categories.id')->where('category_models.category_id', '=', $category_id);
->join('categories', 'category_models.category_id', '=', 'categories.id')
->whereIn('category_models.category_id', (!is_array($category_id) ? explode(',',$category_id): $category_id));
//->whereIn('category_models.category_id', $category_id);
}
/**
@ -1825,7 +1827,7 @@ class Asset extends Depreciable
public function scopeByManufacturer($query, $manufacturer_id)
{
return $query->join('models', 'assets.model_id', '=', 'models.id')
->join('manufacturers', 'models.manufacturer_id', '=', 'manufacturers.id')->where('models.manufacturer_id', '=', $manufacturer_id);
->join('manufacturers', 'models.manufacturer_id', '=', 'manufacturers.id')->whereIn('models.manufacturer_id', (!is_array($manufacturer_id) ? explode(',',$manufacturer_id): $manufacturer_id));
}

View file

@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
use \App\Presenters\AssetModelPresenter;
use App\Http\Traits\TwoColumnUniqueUndeletedTrait;
/**
* Model for Asset Models. Asset Models contain higher level
@ -21,21 +22,8 @@ class AssetModel extends SnipeModel
{
use HasFactory;
use SoftDeletes;
protected $presenter = AssetModelPresenter::class;
use Loggable, Requestable, Presentable;
protected $table = 'models';
protected $hidden = ['user_id', 'deleted_at'];
// Declare the rules for the model validation
protected $rules = [
'name' => 'required|min:1|max:255',
'model_number' => 'max:255|nullable',
'min_amt' => 'integer|min:0|nullable',
'category_id' => 'required|integer|exists:categories,id',
'manufacturer_id' => 'integer|exists:manufacturers,id|nullable',
'eol' => 'integer:min:0|max:240|nullable',
];
use TwoColumnUniqueUndeletedTrait;
/**
* Whether the model should inject its identifier to the unique
@ -44,8 +32,26 @@ class AssetModel extends SnipeModel
*
* @var bool
*/
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
protected $table = 'models';
protected $hidden = ['user_id', 'deleted_at'];
protected $presenter = AssetModelPresenter::class;
// Declare the rules for the model validation
protected $rules = [
'name' => 'string|required|min:1|max:255|two_column_unique_undeleted:model_number',
'model_number' => 'string|max:255|nullable|two_column_unique_undeleted:name',
'min_amt' => 'integer|min:0|nullable',
'category_id' => 'required|integer|exists:categories,id',
'manufacturer_id' => 'integer|exists:manufacturers,id|nullable',
'eol' => 'integer:min:0|max:240|nullable',
];
/**
* The attributes that are mass assignable.
@ -73,7 +79,12 @@ class AssetModel extends SnipeModel
*
* @var array
*/
protected $searchableAttributes = ['name', 'model_number', 'notes', 'eol'];
protected $searchableAttributes = [
'name',
'model_number',
'notes',
'eol'
];
/**
* The relations and their attributes that should be included when searching the model.
@ -86,6 +97,9 @@ class AssetModel extends SnipeModel
'manufacturer' => ['name'],
];
/**
* Establishes the model -> assets relationship
*

View file

@ -205,7 +205,11 @@ class Component extends SnipeModel
public function numCheckedOut()
{
$checkedout = 0;
foreach ($this->assets as $checkout) {
// In case there are elements checked out to assets that belong to a different company
// than this asset and full multiple company support is on we'll remove the global scope,
// so they are included in the count.
foreach ($this->assets()->withoutGlobalScope(new CompanyableScope)->get() as $checkout) {
$checkedout += $checkout->pivot->assigned_qty;
}

View file

@ -76,9 +76,9 @@ class Depreciable extends SnipeModel
if ($months_passed >= $this->get_depreciation()->months){
//if there is a floor use it
if(!$this->get_depreciation()->depreciation_min == null) {
if($this->get_depreciation()->depreciation_min) {
$current_value = $this->get_depreciation()->depreciation_min;
$current_value = $this->calculateDepreciation();
}else{
$current_value = 0;
@ -86,7 +86,7 @@ class Depreciable extends SnipeModel
}
else {
// The equation here is (Purchase_Cost-Floor_min)*(Months_passed/Months_til_depreciated)
$current_value = round(($this->purchase_cost-($this->purchase_cost - ($this->get_depreciation()->depreciation_min)) * ($months_passed / $this->get_depreciation()->months)), 2);
$current_value = round(($this->purchase_cost-($this->purchase_cost - ($this->calculateDepreciation())) * ($months_passed / $this->get_depreciation()->months)), 2);
}
@ -95,7 +95,7 @@ class Depreciable extends SnipeModel
public function getMonthlyDepreciation(){
return ($this->purchase_cost-$this->get_depreciation()->depreciation_min)/$this->get_depreciation()->months;
return ($this->purchase_cost-$this->calculateDepreciation())/$this->get_depreciation()->months;
}
@ -191,4 +191,16 @@ class Depreciable extends SnipeModel
{
return new \DateTime($time);
}
private function calculateDepreciation()
{
if($this->get_depreciation()->depreciation_type === 'percent') {
$depreciation_percent= $this->get_depreciation()->depreciation_min / 100;
$depreciation_min= $this->purchase_cost * $depreciation_percent;
return $depreciation_min;
}
$depreciation_min = $this->get_depreciation()->depreciation_min;
return $depreciation_min;
}
}

View file

@ -75,4 +75,17 @@ class Depreciation extends SnipeModel
{
return $this->hasMany(\App\Models\License::class, 'depreciation_id');
}
/**
* Establishes the depreciation -> assets relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
{
return $this->hasManyThrough(\App\Models\Asset::class, \App\Models\AssetModel::class, 'depreciation_id', 'model_id');
}
}

View file

@ -33,7 +33,7 @@ class Location extends SnipeModel
'country' => 'min:2|max:191|nullable',
'zip' => 'max:10|nullable',
'manager_id' => 'exists:users,id|nullable',
'parent_id' => 'non_circular:locations,id',
'parent_id' => 'nullable|exists:locations,id|non_circular:locations,id',
];
protected $casts = [
@ -108,10 +108,11 @@ class Location extends SnipeModel
{
return Gate::allows('delete', $this)
&& ($this->assets_count === 0)
&& ($this->assigned_assets_count === 0)
&& ($this->children_count === 0)
&& ($this->users_count === 0);
&& ($this->assets_count == 0)
&& ($this->assigned_assets_count == 0)
&& ($this->children_count == 0)
&& ($this->accessories_count == 0)
&& ($this->users_count == 0);
}
/**

View file

@ -74,7 +74,10 @@ class Setting extends Model
'login_remote_user_header_name' => 'string|nullable',
'thumbnail_max_h' => 'numeric|max:500|min:25',
'pwd_secure_min' => 'numeric|required|min:8',
'alert_threshold' => 'numeric|nullable',
'alert_interval' => 'numeric|nullable',
'audit_warning_days' => 'numeric|nullable',
'due_checkin_days' => 'numeric|nullable',
'audit_interval' => 'numeric|nullable',
'custom_forgot_pass_url' => 'url|nullable',
'privacy_policy_link' => 'nullable|url',

View file

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

View file

@ -331,7 +331,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
*/
public function accessories()
{
return $this->belongsToMany(\App\Models\Accessory::class, 'accessories_users', 'assigned_to', 'accessory_id')
return $this->belongsToMany(\App\Models\Accessory::class, 'accessories_checkout', 'assigned_to', 'accessory_id')
->withPivot('id', 'created_at', 'note')->withTrashed()->orderBy('accessory_id');
}

View file

@ -24,6 +24,7 @@ class AcceptanceAssetAcceptedNotification extends Notification
$this->item_tag = $params['item_tag'];
$this->item_model = $params['item_model'];
$this->item_serial = $params['item_serial'];
$this->item_status = $params['item_status'];
$this->accepted_date = Helper::getFormattedDateObject($params['accepted_date'], 'date', false);
$this->assigned_to = $params['assigned_to'];
$this->note = $params['note'];
@ -65,6 +66,7 @@ class AcceptanceAssetAcceptedNotification extends Notification
'item_tag' => $this->item_tag,
'item_model' => $this->item_model,
'item_serial' => $this->item_serial,
'item_status' => $this->item_status,
'note' => $this->note,
'accepted_date' => $this->accepted_date,
'assigned_to' => $this->assigned_to,

View file

@ -24,6 +24,7 @@ class AcceptanceAssetDeclinedNotification extends Notification
$this->item_tag = $params['item_tag'];
$this->item_model = $params['item_model'];
$this->item_serial = $params['item_serial'];
$this->item_status = $params['item_status'];
$this->declined_date = Helper::getFormattedDateObject($params['declined_date'], 'date', false);
$this->note = $params['note'];
$this->assigned_to = $params['assigned_to'];
@ -63,6 +64,7 @@ class AcceptanceAssetDeclinedNotification extends Notification
'item_tag' => $this->item_tag,
'item_model' => $this->item_model,
'item_serial' => $this->item_serial,
'item_status' => $this->item_status,
'note' => $this->note,
'declined_date' => $this->declined_date,
'assigned_to' => $this->assigned_to,

View file

@ -162,6 +162,7 @@ class CheckinAssetNotification extends Notification
$message = (new MailMessage)->markdown('notifications.markdown.checkin-asset',
[
'item' => $this->item,
'status' => $this->item->assetstatus?->name,
'admin' => $this->admin,
'note' => $this->note,
'target' => $this->target,

View file

@ -209,6 +209,7 @@ public function toGoogleChat()
[
'item' => $this->item,
'admin' => $this->admin,
'status' => $this->item->assetstatus?->name,
'note' => $this->note,
'target' => $this->target,
'fields' => $fields,

View file

@ -90,7 +90,7 @@ class AccessoryPresenter extends Presenter
'visible' => false,
'title' => trans('admin/accessories/general.remaining'),
],[
'field' => 'users_count',
'field' => 'checkouts_count',
'searchable' => false,
'sortable' => true,
'visible' => true,

View file

@ -579,6 +579,6 @@ class AssetPresenter extends Presenter
public function glyph()
{
return '<i class="fas fa-barcode" aria-hidden="true"></i>';
return '<x-icon type="assets" />';
}
}

View file

@ -65,40 +65,46 @@ class CompanyPresenter extends Presenter
'field' => 'users_count',
'searchable' => false,
'sortable' => true,
'title' => '<span class="hidden-xs"><i class="fas fa-users"></i></span><span class="hidden-md hidden-lg">'.trans('general.users').'</span></th>',
'title' => trans('general.users'),
'visible' => true,
'class' => 'css-users',
], [
'field' => 'assets_count',
'searchable' => false,
'sortable' => true,
'title' => '<span class="hidden-xs"><i class="fas fa-barcode" aria-hidden="true"></i></span><span class="hidden-md hidden-lg">'.trans('general.assets').'</span>',
'title' => trans('general.assets'),
'visible' => true,
'class' => 'css-barcode',
], [
'field' => 'licenses_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.licenses'),
'visible' => true,
'title' => ' <span class="hidden-xs"><i class="far fa-save"></i></span><span class="hidden-md hidden-lg">'.trans('general.licenses').'</span>',
'class' => 'css-license',
], [
'field' => 'accessories_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.accessories'),
'visible' => true,
'title' => ' <span class="hidden-xs"><i class="far fa-keyboard"></i></span><span class="hidden-md hidden-lg">'.trans('general.accessories').'</span>',
'class' => 'css-accessory',
], [
'field' => 'consumables_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.consumables'),
'visible' => true,
'title' => ' <span class="hidden-xs"><i class="fas fa-tint"></i></span><span class="hidden-md hidden-lg">'.trans('general.consumables').'</span>',
'class' => 'css-consumable',
], [
'field' => 'components_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.components'),
'visible' => true,
'title' => ' <span class="hidden-xs"><i class="far fa-hdd"></i></span><span class="hidden-md hidden-lg">'.trans('general.components').'</span>',
'class' => 'css-component',
], [
'field' => 'updated_at',
'searchable' => false,

View file

@ -46,6 +46,26 @@ class DepreciationPresenter extends Presenter
"title" => trans('admin/depreciations/table.depreciation_min'),
"visible" => true,
],
[
'field' => 'assets_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.assets'),
'visible' => true,
],
[
'field' => 'models_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.asset_models'),
'visible' => true,
], [
'field' => 'licenses_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.licenses'),
'visible' => true,
],
[
'field' => 'actions',
'searchable' => false,

View file

@ -394,6 +394,6 @@ class DepreciationReportPresenter extends Presenter
public function glyph()
{
return '<i class="fas fa-barcode" aria-hidden="true"></i>';
return '<x-icon type="reports" class="text-orange" />';
}
}

View file

@ -235,7 +235,7 @@ class LocationPresenter extends Presenter
public function glyph()
{
return '<i class="fas fa-map-marker-alt" aria-hidden="true"></i>';
return '<x-icon type="locations" />';
}
public function fullName()

View file

@ -94,36 +94,36 @@ class ManufacturerPresenter extends Presenter
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => ' <span class="hidden-md hidden-lg">Assets</span>'
.'<span class="hidden-xs"><i class="fas fa-barcode fa-lg"></i></span>',
'title' => trans('general.assets'),
'visible' => true,
'class' => 'css-barcode',
],
[
'field' => 'licenses_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => ' <span class="hidden-md hidden-lg">Licenses</span>'
.'<span class="hidden-xs"><i class="far fa-save fa-lg"></i></span>',
'title' => trans('general.licenses'),
'visible' => true,
'class' => 'css-license',
],
[
'field' => 'consumables_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => ' <span class="hidden-md hidden-lg">Consumables</span>'
.'<span class="hidden-xs"><i class="fas fa-tint fa-lg"></i></span>',
'title' => trans('general.consumables'),
'visible' => true,
'class' => 'css-consumable',
],
[
'field' => 'accessories_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => ' <span class="hidden-md hidden-lg">Accessories</span>'
.'<span class="hidden-xs"><i class="far fa-keyboard fa-lg"></i></span>',
'title' => trans('general.accessories'),
'visible' => true,
'class' => 'css-accessory',
],
[
'field' => 'created_at',

View file

@ -492,6 +492,6 @@ class UserPresenter extends Presenter
public function glyph()
{
return '<i class="fas fa-user" aria-hidden="true"></i>';
return '<x-icon type="user"/>';
}
}

View file

@ -230,7 +230,8 @@ class AuthServiceProvider extends ServiceProvider
|| $user->can('update', Accessory::class)
|| $user->can('create', Accessory::class)
|| $user->can('update', User::class)
|| $user->can('create', User::class);
|| $user->can('create', User::class)
|| ($user->hasAccess('reports.view'));
});

View file

@ -6,10 +6,7 @@ use App\Models\CustomField;
use App\Models\Department;
use App\Models\Setting;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
/**
@ -91,18 +88,26 @@ class ValidationServiceProvider extends ServiceProvider
*
* $parameters[0] - the name of the first table we're looking at
* $parameters[1] - the ID (this will be 0 on new creations)
* $parameters[2] - the name of the second table we're looking at
* $parameters[2] - the name of the second field we're looking at
* $parameters[3] - the value that the request is passing for the second table we're
* checking for uniqueness across
*
*/
Validator::extend('two_column_unique_undeleted', function ($attribute, $value, $parameters, $validator) {
if (count($parameters)) {
$count = DB::table($parameters[0])
->select('id')->where($attribute, '=', $value)
->whereNull('deleted_at')
->where('id', '!=', $parameters[1])
->where($parameters[2], $parameters[3])->count();
->select('id')
->where($attribute, '=', $value)
->where('id', '!=', $parameters[1]);
if ($parameters[3]!='') {
$count = $count->where($parameters[2], $parameters[3]);
}
$count = $count->whereNull('deleted_at')
->count();
return $count < 1;
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Rules;
use App\Models\Setting;
use App\Models\User;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class UserCannotSwitchCompaniesIfItemsAssigned implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$user = User::find(request()->route('user')->id);
if (($value) && ($user->allAssignedCount() > 0) && (Setting::getSettings()->full_multiple_companies_support)) {
$fail(trans('admin/users/message.error.multi_company_items_assigned'));
}
}
}

View file

@ -337,12 +337,12 @@ class Saml
/**
* Get a setting.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @param string|array|int $key
* @param mixed $default
*
* @return void
* @return mixed
* @author Johnson Yi <jyi.dev@outlook.com>
*
*/
public function getSetting($key, $default = null)
{

View file

@ -107,7 +107,7 @@ class Label implements View
if ($settings->alt_barcode_enabled) {
if ($template->getSupport1DBarcode()) {
$barcode1DType = $settings->alt_barcode;
$barcode1DType = $settings->label2_1d_type;
if ($barcode1DType != 'none') {
$assetData->put('barcode1d', (object)[
'type' => $barcode1DType,

View file

@ -52,7 +52,7 @@
"livewire/livewire": "^3.5",
"neitanod/forceutf8": "^2.0",
"nesbot/carbon": "^2.32",
"nunomaduro/collision": "^6.1",
"nunomaduro/collision": "^7.0",
"okvpn/clock-lts": "^1.0",
"onelogin/php-saml": "^3.4",
"paragonie/constant_time_encoding": "^2.3",
@ -74,13 +74,13 @@
"ext-exif": "*"
},
"require-dev": {
"brianium/paratest": "^v6.4.4",
"brianium/paratest": "^7.0",
"fakerphp/faker": "^1.16",
"larastan/larastan": "^2.9",
"mockery/mockery": "^1.4",
"nunomaduro/phpinsights": "^2.7",
"nunomaduro/phpinsights": "^2.11",
"php-mock/php-mock-phpunit": "^2.10",
"phpunit/phpunit": "^9.6.19",
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "^3.5",
"symfony/css-selector": "^4.4",
"symfony/dom-crawler": "^4.4",

1218
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -372,7 +372,9 @@ return [
'Google2FA' => PragmaRX\Google2FALaravel\Facade::class,
'Image' => Intervention\Image\ImageServiceProvider::class,
'Carbon' => Carbon\Carbon::class,
'Helper' => App\Helpers\Helper::class, // makes it much easier to use 'Helper::blah' in blades (which is where we usually use this)
'Helper' => App\Helpers\Helper::class,
// makes it much easier to use 'Helper::blah' in blades (which is where we usually use this)
'Icon' => App\Helpers\IconHelper::class,
'Socialite' => Laravel\Socialite\Facades\Socialite::class,

View file

@ -237,4 +237,6 @@ return [
],
],
'sanitize_by_default' => env('DB_SANITIZE_BY_DEFAULT', false),
];

View file

@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v7.0.9',
'full_app_version' => 'v7.0.9 - build 14485-ga98ad76c6',
'build_version' => '14485',
'app_version' => 'v7.0.11',
'full_app_version' => 'v7.0.11 - build 15044-g46ed07642',
'build_version' => '15044',
'prerelease_version' => '',
'hash_version' => 'ga98ad76c6',
'full_hash' => 'v7.0.9-112-ga98ad76c6',
'hash_version' => 'g46ed07642',
'full_hash' => 'v7.0.11-133-g46ed07642',
'branch' => 'develop',
);

View file

@ -3,6 +3,7 @@
namespace Database\Factories;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Category;
use App\Models\Location;
use App\Models\Manufacturer;
@ -125,11 +126,12 @@ class AccessoryFactory extends Factory
})->afterCreating(function ($accessory) {
$user = User::factory()->create();
$accessory->users()->attach($accessory->id, [
$accessory->checkouts()->create([
'accessory_id' => $accessory->id,
'created_at' => now(),
'created_at' => Carbon::now(),
'user_id' => $user->id,
'assigned_to' => $user->id,
'assigned_type' => User::class,
'note' => '',
]);
});
@ -145,11 +147,12 @@ class AccessoryFactory extends Factory
public function checkedOutToUser(User $user = null)
{
return $this->afterCreating(function (Accessory $accessory) use ($user) {
$accessory->users()->attach($accessory->id, [
$accessory->checkouts()->create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => 1,
'assigned_to' => $user->id ?? User::factory()->create()->id,
'assigned_type' => User::class,
]);
});
}

View file

@ -296,6 +296,11 @@ class UserFactory extends Factory
return $this->appendPermission(['reports.view' => '1']);
}
public function canImport()
{
return $this->appendPermission(['import' => '1']);
}
private function appendPermission(array $permission)
{
return $this->state(function ($currentState) use ($permission) {

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('depreciations', function (Blueprint $table) {
$table->string('depreciation_type')->after('depreciation_min')->default('amount');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('depreciations', function (Blueprint $table) {
$table->dropColumn('depreciation_type');
});
}
};

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