Compare commits

...

102 Commits

Author SHA1 Message Date
snipe
023bf32dca First stab 2025-10-01 13:48:21 +01:00
snipe
9404dff79c Added note about markdown 2025-10-01 12:12:56 +01:00
snipe
3ed2e2d79e Added link to docs for common issues in upgrading script 2025-10-01 11:49:05 +01:00
snipe
f1266ab5d6 Check if telescope tables already exist 2025-10-01 11:24:20 +01:00
snipe
664e3984e3 Merge pull request #17877 from grokability/label-cjk-fix
Fixed CJK on labels
2025-10-01 11:16:55 +01:00
snipe
665c13e238 Merge pull request #17957 from marcusmoore/fixes/17956-handle-force-deleted-model-in-asset-edit
Fixed #17956 - handle accessing deleted model during asset update
2025-10-01 11:10:11 +01:00
snipe
8a667b20c2 Merge branch 'develop' into fixes/17956-handle-force-deleted-model-in-asset-edit 2025-10-01 11:09:40 +01:00
snipe
3693241292 Merge pull request #17959 from marcusmoore/fixes/17958-handle-force-deleted-model-in-bulk-edit
Fixes #17958 - handle accessing deleted model during bulk asset update
2025-10-01 11:06:32 +01:00
Marcus Moore
3c3acff79b Fix more attempted access of deleted model 2025-09-30 12:22:26 -07:00
Marcus Moore
e15de83a95 Fix attempted access of deleted model 2025-09-30 12:19:12 -07:00
Marcus Moore
636fccbf97 Add failing test 2025-09-30 12:18:51 -07:00
Marcus Moore
7d8ed399a8 Fix accessing force deleted model 2025-09-30 11:27:56 -07:00
Marcus Moore
272385db6c Add failing test 2025-09-30 11:27:38 -07:00
snipe
291be64aa0 Refined remnaining asset count for archived 2025-09-30 11:40:21 +01:00
snipe
72be171917 Added archived to model view 2025-09-30 11:20:33 +01:00
snipe
43cd0d7eb3 Use min_amt formatter 2025-09-30 10:54:43 +01:00
snipe
eeea69d8f2 Make available_assets_count sortable 2025-09-30 10:51:25 +01:00
snipe
bec88a0441 Merge pull request #17950 from grokability/#17932-fix-remaining-counts-in-model-listing
Fixed #17932 - incorrect number for remaining assets in asset models
2025-09-29 20:55:52 +01:00
snipe
6e67e3a8a0 Fixed #17932 - incorrect number for remaining assets in asset models 2025-09-29 20:55:06 +01:00
snipe
947ccf911d Merge pull request #17868 from Godmartinz/adds-Tze_24mm-variant
Adds Brother Label TZe_24mm_E variant
2025-09-29 15:48:10 +01:00
snipe
06f313febe Merge pull request #17869 from marcusmoore/api-components-assigned-to-asset
Added api endpoint for retrieving components checked out to asset
2025-09-29 15:47:53 +01:00
snipe
b387136b8f Merge pull request #17883 from akemidx/purchasepricereportfilter
FEATURE: Purchase Cost Report Filter
2025-09-29 15:37:39 +01:00
snipe
31614c5da1 Merge pull request #17888 from marcusmoore/fixes/bulk-checkout-extra-requests
Fixed excessive api requests on bulk checkout page
2025-09-29 14:46:19 +01:00
snipe
146b5a3085 Merge pull request #17933 from marcusmoore/17914-bulk-checkout-error-ux
Fixed #17914 - Improve UX around attempted bulk checkout of assigned assets
2025-09-29 14:44:20 +01:00
snipe
ff1297cac5 Merge pull request #17945 from kingspride/develop
with --no-interactive, make composer non-interactive aswell
2025-09-29 11:04:30 +01:00
William Kirstaedter
8af3cf4056 with --no-interactive, make composer non-interactive aswell 2025-09-29 11:39:23 +02:00
Marcus Moore
9edec9e212 Extract translation 2025-09-25 11:09:27 -07:00
snipe
be4362c59a Merge pull request #17925 from Godmartinz/fix-factory-auto-gen-action-logs
Adds option to disable auto generating action log from acceptance factory
2025-09-25 11:10:34 +01:00
Marcus Moore
8461b147de Link to removed assets 2025-09-24 16:38:48 -07:00
Godfrey M
82bdd43168 renamed variable 2025-09-24 15:38:30 -07:00
Godfrey M
533d82d4d8 remove unnecessary changes 2025-09-24 15:34:02 -07:00
Godfrey M
6f990dd1de adds an option to disable Auto assigned an actionlogs in factories 2025-09-24 15:27:16 -07:00
Marcus Moore
be848598e3 Keep removed asset out of scope of partial 2025-09-24 14:32:25 -07:00
snipe
7d0742054f Merge pull request #17923 from uberbrady/fix_checkout_type_selector2
Fixed #17919 - correct the behavior of the checkout type selector
2025-09-24 14:52:05 +01:00
Brady Wetherington
dcf7e83507 Remove extra pointless class="active" 2025-09-24 14:47:13 +01:00
Brady Wetherington
407c2bf0c8 Switch to ?: from ?? to better handle empty strings 2025-09-24 14:45:43 +01:00
Brady Wetherington
c46227ee94 Fix to the checkout-selector issue 2025-09-24 14:28:49 +01:00
snipe
d8171eb056 Remove duplicate PUT route for hardware assets
Removed duplicate route definition for updating hardware assets.
2025-09-23 13:24:56 +01:00
Marcus Moore
c614c44d4c Remove assigned assets from bulk checkout 2025-09-22 16:04:29 -07:00
snipe
8a46579588 Merge pull request #17887 from marcusmoore/fixes/17404-prevent-bulk-checkout-across-companies
Fixed #17404 - Disallow bulk checkout of assets across companies
2025-09-22 18:57:43 +01:00
Marcus Moore
fb9fb9c097 Merge branch 'develop' into fixes/17404-prevent-bulk-checkout-across-companies
# Conflicts:
#	app/Http/Controllers/Assets/BulkAssetsController.php
#	tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php
2025-09-22 10:52:04 -07:00
snipe
d9399534ce Merge pull request #17909 from spacjalex/17908-fix-typo
Fix #17908: typo in storage location of backups
2025-09-22 14:28:05 +01:00
spacjalex
17a749bbed fix typo 2025-09-22 15:23:14 +02:00
snipe
25ce63f00b Merge pull request #17904 from grokability/#17804-searchable-columns
Fixed #17804 - make columns searchable in column picker
2025-09-19 12:43:46 +01:00
snipe
2462bc05b3 Added column search to additional views 2025-09-19 12:41:30 +01:00
snipe
c3748da0b1 Fixed #17804 - make columns searchable in column picker 2025-09-19 12:16:56 +01:00
snipe
90c242a441 Merge pull request #17897 from marcusmoore/fixes/17896-prevent-bulk-checkout-of-checked-out-assets
Fixed #17896 - Prevent assigned assets from being bulk checked out
2025-09-19 07:03:43 +01:00
Marcus Moore
52239a88b5 Improve test name 2025-09-18 17:27:17 -07:00
Marcus Moore
7a3596c86d Test against other types 2025-09-18 17:21:27 -07:00
Marcus Moore
ac8a9e38f0 Implement fix 2025-09-18 17:18:27 -07:00
Marcus Moore
5c08f3a27e Add failing test 2025-09-18 17:14:33 -07:00
Marcus Moore
2dc11a84bf Fix test name 2025-09-18 17:05:08 -07:00
Marcus Moore
2960ea15f5 Consolidate to data provider 2025-09-18 14:29:12 -07:00
Marcus Moore
17aab4c490 Implement test 2025-09-18 14:20:05 -07:00
Marcus Moore
59d0f0d292 Re-order assertions 2025-09-18 14:05:13 -07:00
Marcus Moore
27d13a113a Implement test 2025-09-18 14:01:44 -07:00
Marcus Moore
c58e999fbb Scaffold tests 2025-09-18 13:11:06 -07:00
Marcus Moore
a02a96d5c4 Extract translation string 2025-09-18 12:57:56 -07:00
Marcus Moore
47e9e4704d Improve error message 2025-09-18 12:56:36 -07:00
Marcus Moore
b2ad9d404e Fix re-population of assets 2025-09-18 12:38:11 -07:00
snipe
5216dd75bf Bumped version 2025-09-18 14:49:15 +01:00
snipe
b8b45d2d81 Merge pull request #17892 from grokability/#17891-fixes-maintenance-file-route
Fixed #17891 - missing maintenance file deletion route
2025-09-18 13:59:10 +01:00
snipe
4b2b2cb68e Fixed #17891 - missing maintenance file deletion route 2025-09-18 13:58:30 +01:00
snipe
be4ace293e Use trans_choice for user acceptance 2025-09-18 13:51:57 +01:00
snipe
764b363bbc A few small tweaks to acceptance screen design 2025-09-18 13:38:37 +01:00
Marcus Moore
705474dc14 Avoid pre-loading all assets on page load 2025-09-17 16:56:37 -07:00
Marcus Moore
e639d7726b Disallow bulk checkout across companies 2025-09-17 14:32:27 -07:00
snipe
9da9166442 Merge pull request #17886 from grokability/small-tweaks-to-acceptance-pdf
Small adjustments for acceptance PDF layout
2025-09-17 22:01:41 +01:00
snipe
8ea339f0ef More small tweaks 2025-09-17 22:00:49 +01:00
Marcus Moore
e29b0aa6a4 Add todo 2025-09-17 13:55:54 -07:00
Marcus Moore
d2157868f2 Populate failing test 2025-09-17 13:49:32 -07:00
snipe
89b36ba63f Derp. Uncomment the acceptance. 2025-09-17 21:43:40 +01:00
snipe
1d3dfa1fa4 Pull the acceptance stuff into the model 2025-09-17 21:43:17 +01:00
Marcus Moore
89cfafd933 Scaffold test 2025-09-17 13:40:34 -07:00
snipe
ca567eec8a Small adjustments for layout 2025-09-17 21:08:13 +01:00
snipe
41da31c379 Merge pull request #17885 from grokability/#8859-show-cost-footer-on-models
Fixed #8859 - adds purchase sums on model view
2025-09-17 14:04:38 +01:00
snipe
e81f63f46b Fixed #8859 - adds purchase sums on model view 2025-09-17 14:03:48 +01:00
snipe
ade03e4827 Merge pull request #17882 from Godmartinz/add-total-cost-columns
Adds total cost to Accessories, Consumables, Components
2025-09-17 13:56:08 +01:00
snipe
33a4c88c3a Added table to deleted_at clauses to resolve ambiguity 2025-09-17 11:44:28 +01:00
akemidx
69c5dbfc23 formatting 2025-09-17 05:39:45 -04:00
akemidx
cf1bccfd65 prep for val 2025-09-16 15:24:44 -04:00
akemidx
99acf018f1 validation rule & query 2025-09-16 15:17:59 -04:00
snipe
1f79776b8f Pull HTML tags out before converting markdown 2025-09-16 19:21:39 +01:00
Godfrey M
11e5f851f0 typo 2025-09-16 10:49:33 -07:00
Godfrey M
4ca1db8a1b remove footer formatter from consumable purchase cost 2025-09-16 10:43:02 -07:00
Godfrey M
14b829aa30 add total cost to components and consumables 2025-09-16 10:37:32 -07:00
Godfrey M
384652b3df add total cost to accessories 2025-09-16 10:10:49 -07:00
snipe
9db65c6ae9 Merge pull request #17881 from grokability/#17873-eula-tab-on-users
Fixed #17873 - Added EULA tab to user view
2025-09-16 14:28:50 +01:00
snipe
1346e33e99 Check that the person trying to download can see both the user and the target 2025-09-16 14:21:03 +01:00
snipe
ab9cc447aa Use more specific filename 2025-09-16 14:20:20 +01:00
akemidx
cb63c12d2f i think this is gonna need livewire to validate lol 2025-09-16 08:24:22 -04:00
snipe
fe9e0444b4 Added EULA tab to user view 2025-09-16 13:20:50 +01:00
akemidx
6ce0fd20ce works, needs error handling 2025-09-16 08:11:42 -04:00
snipe
a18957dbe9 Include output even if there is nothing to send 2025-09-16 12:33:06 +01:00
snipe
13d5b724ee Fixed tests 2025-09-16 12:16:18 +01:00
Marcus Moore
06f060161d Apply offset and limit 2025-09-15 15:43:54 -07:00
Marcus Moore
73e0628124 Populate test 2025-09-15 15:26:30 -07:00
Marcus Moore
7393c4170b Apply first pass and scaffold additional test 2025-09-15 15:22:35 -07:00
Marcus Moore
73e185bf9d Scaffold route and tests 2025-09-15 15:12:05 -07:00
Godfrey M
77153c3e78 adds Tze_24mm variant 2025-09-15 11:20:35 -07:00
akemidx
50e210b2db fixing naming convention to match 2025-09-10 17:35:09 -04:00
akemidx
b1de98f05d first front end 2025-09-09 19:18:29 -04:00
82 changed files with 21938 additions and 367 deletions

View File

@@ -56,7 +56,7 @@ class SendExpirationAlerts extends Command
$assets = Asset::getExpiringWarrantyOrEol($alert_interval);
if ($assets->count() > 0) {
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
$this->table(
@@ -68,7 +68,6 @@ class SendExpirationAlerts extends Command
// Expiring licenses
$licenses = License::getExpiringLicenses($alert_interval);
if ($licenses->count() > 0) {
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval));
$this->table(
@@ -77,6 +76,10 @@ class SendExpirationAlerts extends Command
);
}
// Send a message even if the count is 0
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
} else {

View File

@@ -95,7 +95,7 @@ class Helper
$Parsedown->setSafeMode(true);
if ($str) {
return $Parsedown->text($str);
return $Parsedown->text(strip_tags($str));
}
}
@@ -105,7 +105,7 @@ class Helper
$Parsedown->setSafeMode(true);
if ($str) {
return $Parsedown->line($str);
return $Parsedown->line(strip_tags($str));
}
}

View File

@@ -13,11 +13,6 @@ use App\Models\Company;
use App\Models\Contracts\Acceptable;
use App\Models\Setting;
use App\Models\User;
use App\Models\AssetModel;
use App\Models\Accessory;
use App\Models\License;
use App\Models\Component;
use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
@@ -26,12 +21,9 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
use Carbon\Carbon;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use TCPDF;
use App\Helpers\Helper;
class AcceptanceController extends Controller
@@ -83,6 +75,11 @@ class AcceptanceController extends Controller
public function store(Request $request, $id) : RedirectResponse
{
$acceptance = CheckoutAcceptance::find($id);
$assigned_user = User::find($acceptance->assigned_to_id);
$settings = Setting::getSettings();
$path_logo = '';
$sig_filename='';
if (is_null($acceptance)) {
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
@@ -118,136 +115,62 @@ class AcceptanceController extends Controller
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
}
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
$display_model = '';
$pdf_view_route = '';
$pdf_filename = 'accepted-eula-'.date('Y-m-d-h-i-s').'.pdf';
$sig_filename='';
// If signatures are required, make sure we have one
if (Setting::getSettings()->require_accept_signature == '1') {
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
// Get the data array ready for the notifications and PDF generation
$data = [
'item_tag' => $item->asset_tag,
'item_name' => $item->name, // this handles licenses seats, which don't have a 'name' field
'item_model' => $item->model?->name,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'eula' => $item->getEula(),
'note' => $request->input('note'),
'check_out_date' => Helper::getFormattedDateObject($acceptance->created_at, 'datetime', false),
'accepted_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false),
'declined_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false),
'assigned_to' => $assigned_user->display_name,
'site_name' => $settings->site_name,
'company_name' => $item->company?->name?? $settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => ($settings->acceptance_pdf_logo) ? public_path() . '/uploads/' . $settings->acceptance_pdf_logo : null,
'date_settings' => $settings->date_display_format,
'admin' => auth()->user()->present()?->fullName,
'qty' => $acceptance->qty ?? 1,
];
if ($request->input('asset_acceptance') == 'accepted') {
if (Setting::getSettings()->require_accept_signature == '1') {
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
$assigned_user = User::find($acceptance->assigned_to_id);
/**
* Gather the data for the PDF. We fire this whether there is a signature required or not,
* since we want the moment-in-time proof of what the EULA was when they accepted it.
*/
$branding_settings = SettingsController::getPDFBranding();
$path_logo = "";
// Check for the PDF logo path and use that, otherwise use the regular logo path
if (!is_null($branding_settings->acceptance_pdf_logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->acceptance_pdf_logo;
} elseif (!is_null($branding_settings->logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->logo;
}
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $display_model,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'eula' => $item->getEula(),
'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d H:i:s'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d H:i:s'),
'assigned_to' => $assigned_user->display_name,
'company_name' => $branding_settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
'date_settings' => $branding_settings->date_display_format,
'admin' => auth()->user()->present()?->fullName,
'qty' => $acceptance->qty ?? 1,
];
// set some language dependent data:
$lg = Array();
$lg['a_meta_charset'] = 'UTF-8';
$lg['w_page'] = 'page';
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->setRTL(false);
$pdf->setLanguageArray($lg);
$pdf->SetFontSubsetting(true);
$pdf->SetCreator('Snipe-IT');
$pdf->SetAuthor($data['assigned_to']);
$pdf->SetTitle('Asset Acceptance: '.$data['item_tag']);
$pdf->SetSubject('Asset Acceptance: '.$data['item_tag']);
$pdf->SetKeywords('Snipe-IT, assets, acceptance, eula', 'tos');
$pdf->SetFont('dejavusans', '', 8, '', true);
$pdf->SetPrintHeader(false);
$pdf->SetPrintFooter(false);
$pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN));
$pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA));
$pdf->AddPage();
$pdf->writeHTML('<img src="'.$path_logo.'" height="30">', true, 0, true, 0, '');
if ($data['item_serial']) {
$pdf->writeHTML("<strong>" . trans('general.asset_tag') . '</strong>: ' . $data['item_tag'], true, 0, true, 0, '');
}
$pdf->writeHTML("<strong>".trans('general.asset_model').'</strong>: '.$data['item_model'], true, 0, true, 0, '');
if ($data['item_serial']) {
$pdf->writeHTML("<strong>".trans('admin/hardware/form.serial').'</strong>: '.$data['item_serial'], true, 0, true, 0, '');
}
$pdf->writeHTML("<strong>".trans('general.assigned_date').'</strong>: '.$data['check_out_date'], true, 0, true, 0, '');
$pdf->writeHTML("<strong>".trans('general.assignee').'</strong>: '.$data['assigned_to'], true, 0, true, 0, '');
$pdf->Ln();
// Break the EULA into lines based on newlines, and check each line for RTL or CJK characters
$eula_lines = preg_split("/\r\n|\n|\r/", $item->getEula());
foreach ($eula_lines as $eula_line) {
Helper::hasRtl($eula_line) ? $pdf->setRTL(true) : $pdf->setRTL(false);
Helper::isCjk($eula_line) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
$pdf->writeHTML(Helper::parseEscapedMarkedown($eula_line), true, 0, true, 0, '');
}
$pdf->Ln();
$pdf->Ln();
$pdf->setRTL(false);
$pdf->writeHTML('<br><br>', true, 0, true, 0, '');
if ($data['note'] != null) {
Helper::isCjk($data['note']) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
$pdf->writeHTML("<strong>".trans('general.notes') . '</strong>: ' . $data['note'], true, 0, true, 0, '');
$pdf->Ln();
}
if ($data['signature'] != null) {
$pdf->writeHTML('<img src="'.$data['signature'].'" style="max-width: 600px;">', true, 0, true, 0, '');
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
}
$pdf->writeHTML("<strong>".trans('general.accepted_date').'</strong>: '.$data['accepted_date'], true, 0, true, 0, '');
$pdf_content = $pdf->Output($pdf_filename, 'S');
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
// Generate the PDF content
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf_content);
// Log the acceptance
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
// Send the PDF to the signing user
@@ -270,45 +193,9 @@ class AcceptanceController extends Controller
$return_msg = trans('admin/users/message.accepted');
// Item was not accepted
// Item was declined
} else {
if (Setting::getSettings()->require_accept_signature == '1') {
// The item was declined, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
// Format the data to send the declined notification
$branding_settings = SettingsController::getPDFBranding();
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $item->model ? $item->model->name : $item->display_name,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'note' => $request->input('note'),
'declined_date' => Carbon::parse($acceptance->declined_at)->format('Y-m-d'),
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'assigned_to' => $assigned_to,
'company_name' => $branding_settings->site_name,
'date_settings' => $branding_settings->date_display_format,
'qty' => $acceptance->qty ?? 1,
];
for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) {
$acceptance->decline($sig_filename, $request->input('note'));
}
@@ -319,6 +206,8 @@ class AcceptanceController extends Controller
$return_msg = trans('admin/users/message.declined');
}
// Send an email notification if one is requested
if ($acceptance->alert_on_response_id) {
try {
$recipient = User::find($acceptance->alert_on_response_id);
@@ -337,9 +226,10 @@ class AcceptanceController extends Controller
Log::warning($e);
}
}
return redirect()->to('account/accept')->with('success', $return_msg);
}
}

View File

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

View File

@@ -46,6 +46,9 @@ class AssetModelsController extends Controller
'manufacturer',
'requestable',
'assets_count',
'assets_assigned_count',
'assets_archived_count',
'remaining',
'category',
'fieldset',
'deleted_at',
@@ -73,7 +76,10 @@ class AssetModelsController extends Controller
'models.require_serial'
])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
->withCount('assets as assets_count');
->withCount('assets as assets_count')
->withCount('availableAssets as remaining')
->withCount('assignedAssets as assets_assigned_count')
->withCount('archivedAssets as assets_archived_count');
if ($request->input('status')=='deleted') {
$assetmodels->onlyTrashed();

View File

@@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn;
use App\Http\Requests\StoreAssetRequest;
use App\Http\Requests\UpdateAssetRequest;
use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Http\Transformers\ComponentsTransformer;
use App\Models\AccessoryCheckout;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
@@ -1322,6 +1323,18 @@ class AssetsController extends Controller
return (new AssetsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total);
}
public function assignedComponents(Request $request, Asset $asset): JsonResponse|array
{
$this->authorize('view', Asset::class);
$this->authorize('view', $asset);
$asset->loadCount('components');
$total = $asset->components_count;
$components = $asset->load(['components' => fn($query) => $query->applyOffsetAndLimit($total)])->components;
return (new ComponentsTransformer)->transformComponents($components, $total);
}
/**
* Generate asset labels by tag

View File

@@ -363,7 +363,7 @@ class AssetsController extends Controller
$asset->purchase_cost = $request->input('purchase_cost', null);
$asset->purchase_date = $request->input('purchase_date', null);
$asset->next_audit_date = $request->input('next_audit_date', null);
if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model->eol > 0)) {
if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model?->eol > 0)) {
$asset->purchase_date = $request->input('purchase_date', null);
$asset->asset_eol_date = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d');
$asset->eol_explicit = false;
@@ -379,7 +379,7 @@ class AssetsController extends Controller
} else {
$asset->eol_explicit = true;
}
} elseif (!$request->filled('asset_eol_date') && (($asset->model->eol) == 0)) {
} elseif (!$request->filled('asset_eol_date') && (($asset->model?->eol) == 0)) {
$asset->asset_eol_date = null;
$asset->eol_explicit = false;
}

View File

@@ -163,7 +163,7 @@ class BulkAssetsController extends Controller
$modelNames = [];
foreach($models as $model) {
$modelNames[] = $model->model->name;
$modelNames[] = $model->model?->name;
}
if ($request->filled('bulk_actions')) {
@@ -470,7 +470,7 @@ class BulkAssetsController extends Controller
*/
// Does the model have a fieldset?
if ($asset->model->fieldset) {
if ($asset->model?->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
// null custom fields
@@ -621,9 +621,25 @@ class BulkAssetsController extends Controller
{
$this->authorize('checkout', Asset::class);
$alreadyAssigned = collect();
if (old('selected_assets') && is_array(old('selected_assets'))) {
$assets = Asset::findMany(old('selected_assets'));
[$assignable, $alreadyAssigned] = $assets->partition(function (Asset $asset) {
return !$asset->assigned_to;
});
session()->flashInput(['selected_assets' => $assignable->pluck('id')->values()->toArray()]);
}
$do_not_change = ['' => trans('general.do_not_change')];
$status_label_list = $do_not_change + Helper::deployableStatusLabelList();
return view('hardware/bulk-checkout')->with('statusLabel_list', $status_label_list);
return view('hardware/bulk-checkout', [
'statusLabel_list' => $status_label_list,
'removed_assets' => $alreadyAssigned,
]);
}
/**
@@ -647,6 +663,30 @@ class BulkAssetsController extends Controller
$assets = Asset::findOrFail($asset_ids);
// Prevent checking out assets that are already checked out
if ($assets->pluck('assigned_to')->unique()->filter()->isNotEmpty()) {
// re-add the asset ids so the assets select is re-populated
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect(route('hardware.bulkcheckout.show'))
->with('error', trans('general.error_assets_already_checked_out'));
}
// Prevent checking out assets across companies if FMCS enabled
if (Setting::getSettings()->full_multiple_companies_support && $target->company_id) {
$company_ids = $assets->pluck('company_id')->unique();
// if there is more than one unique company id or the singular company id does not match
// then the checkout is invalid
if ($company_ids->count() > 1 || $company_ids->first() != $target->company_id) {
// re-add the asset ids so the assets select is re-populated
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect(route('hardware.bulkcheckout.show'))
->with('error', trans('general.error_user_company_multiple'));
}
}
if (request('checkout_to_type') == 'asset') {
foreach ($asset_ids as $asset_id) {
if ($target->id == $asset_id) {

View File

@@ -685,6 +685,14 @@ class ReportsController extends Controller
$assets->whereBetween('assets.purchase_date', [$request->input('purchase_start'), $request->input('purchase_end')]);
}
if ($request->filled('purchase_cost_start')) {
if ($request->filled('purchase_cost_end')) {
$assets->whereBetween('assets.purchase_cost', [$request->input('purchase_cost_start'), $request->input('purchase_cost_end')]);
} else {
$assets->where('assets.purchase_cost', ">", $request->input('purchase_cost_start'));
}
}
if (($request->filled('created_start')) && ($request->filled('created_end'))) {
$created_start = Carbon::parse($request->input('created_start'))->startOfDay();
$created_end = Carbon::parse($request->input('created_end'))->endOfDay();

View File

@@ -14,6 +14,15 @@ class CustomAssetReportRequest extends Request
return true;
}
public function prepareForValidation()
{
if($this->filled('purchase_cost_end') && !$this->filled('purchase_cost_start')){
$this->merge(['purchase_cost_start' => 0 ]);
}
}
/**
* Get the validation rules that apply to the request.
*
@@ -24,6 +33,7 @@ class CustomAssetReportRequest extends Request
return [
'purchase_start' => 'date|date_format:Y-m-d|nullable',
'purchase_end' => 'date|date_format:Y-m-d|nullable',
'purchase_cost_end' => 'numeric|nullable|gte:purchase_cost_start',
'created_start' => 'date|date_format:Y-m-d|nullable',
'created_end' => 'date|date_format:Y-m-d|nullable',
'checkout_date_start' => 'date|date_format:Y-m-d|nullable',

View File

@@ -36,6 +36,7 @@ class AccessoriesTransformer
'qty' => ($accessory->qty) ? (int) $accessory->qty : null,
'purchase_date' => ($accessory->purchase_date) ? Helper::getFormattedDateObject($accessory->purchase_date, 'date') : null,
'purchase_cost' => Helper::formatCurrencyOutput($accessory->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($accessory->totalCostSum()),
'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null,
'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null, // Legacy - should phase out - replaced by below, for the bootstrap table formatter
'min_amt' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,

View File

@@ -147,7 +147,7 @@ class ActionlogsTransformer
[
'url' => $actionlog->uploads_file_url(),
'filename' => $actionlog->filename,
'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_url()),
'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_path()),
'exists_on_disk' => Storage::exists($actionlog->uploads_file_path()) ? true : false,
] : null,

View File

@@ -48,12 +48,15 @@ class AssetModelsTransformer
'image' => ($assetmodel->image != '') ? Storage::disk('public')->url('models/'.e($assetmodel->image)) : null,
'model_number' => ($assetmodel->model_number ? e($assetmodel->model_number): null),
'min_amt' => ($assetmodel->min_amt) ? (int) $assetmodel->min_amt : null,
'remaining' => (int) ($assetmodel->assets_count - $assetmodel->min_amt),
'depreciation' => ($assetmodel->depreciation) ? [
'id' => (int) $assetmodel->depreciation->id,
'name'=> e($assetmodel->depreciation->name),
] : null,
'assets_count' => (int) $assetmodel->assets_count,
'assets_assigned_count' => (int) $assetmodel->assets_assigned_count,
'assets_archived_count' => (int) $assetmodel->assets_archived_count,
'remaining' => (int) ($assetmodel->assets_count - (int) $assetmodel->assets_assigned_count) - (int) $assetmodel->assets_archived_count,
'category' => ($assetmodel->category) ? [
'id' => (int) $assetmodel->category->id,
'name'=> e($assetmodel->category->name),

View File

@@ -43,6 +43,7 @@ class ComponentsTransformer
'order_number' => e($component->order_number),
'purchase_date' => Helper::getFormattedDateObject($component->purchase_date, 'date'),
'purchase_cost' => Helper::formatCurrencyOutput($component->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($component->totalCostSum()),
'remaining' => (int) $component->numRemaining(),
'company' => ($component->company) ? [
'id' => (int) $component->company->id,

View File

@@ -37,6 +37,7 @@ class ConsumablesTransformer
'remaining' => $consumable->numRemaining(),
'order_number' => e($consumable->order_number),
'purchase_cost' => Helper::formatCurrencyOutput($consumable->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($consumable->totalCostSum()),
'purchase_date' => Helper::getFormattedDateObject($consumable->purchase_date, 'date'),
'qty' => (int) $consumable->qty,
'notes' => ($consumable->notes) ? Helper::parseEscapedMarkedownInline($consumable->notes) : null,

View File

@@ -357,6 +357,10 @@ class Accessory extends SnipeModel
$accessory_checkout->limit(1)->delete();
}
public function totalCostSum() {
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
}
/**
* -----------------------------------------------

View File

@@ -122,6 +122,22 @@ class AssetModel extends SnipeModel
return $this->hasMany(\App\Models\Asset::class, 'model_id');
}
public function availableAssets()
{
return $this->hasMany(\App\Models\Asset::class, 'model_id')->RTD();
}
public function assignedAssets()
{
return $this->hasMany(\App\Models\Asset::class, 'model_id')->Deployed();
}
public function archivedAssets()
{
return $this->hasMany(\App\Models\Asset::class, 'model_id')->Archived();
}
/**
* Establishes the model -> category relationship
*

View File

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

View File

@@ -288,7 +288,10 @@ class Component extends SnipeModel
return $this->qty - $this->numCheckedOut();
}
public function totalCostSum() {
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS

View File

@@ -312,7 +312,10 @@ class Consumable extends SnipeModel
return $remaining;
}
public function totalCostSum() {
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Models\Labels\Tapes\Brother;
class TZe_24mm_E extends TZe_24mm
{
private const BARCODE_MARGIN = 1.50;
private const TAG_SIZE = 2.00;
private const TITLE_SIZE = 2.80;
private const TITLE_MARGIN = 0.50;
private const LABEL_SIZE = 2.00;
private const LABEL_MARGIN = - 0.35;
private const FIELD_SIZE = 2.80;
private const FIELD_MARGIN = 0.15;
private const BARCODE1D_SIZE = - 1.00;
public function getUnit() { return 'mm'; }
public function getWidth() { return 45.0; }
public function getHeight() { return 15; }
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 3; }
public function getSupportLogo() { return false; }
public function getSupportTitle() { return true; }
public function preparePDF($pdf) {}
public function write($pdf, $record) {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
$currentY = $pa->y1;
$usableWidth = $pa->w;
$usableHeight = $pa->h - self::BARCODE1D_SIZE;
$barcodeSize = $usableHeight - self::TAG_SIZE;
if ($record->has('barcode2d')) {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE - self::BARCODE1D_SIZE,
'freesans', 'b', self::TAG_SIZE, 'C',
$barcodeSize, self::TAG_SIZE, true, 0
);
static::write2DBarcode(
$pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
$currentX, $currentY,
$barcodeSize, $barcodeSize
);
$currentX += $barcodeSize + self::BARCODE_MARGIN;
$usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
} else {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE - self::BARCODE1D_SIZE,
'freesans', 'B', self::TAG_SIZE, 'R',
$usableWidth, self::TAG_SIZE, true, 0
);
}
if ($record->has('title')) {
static::writeText(
$pdf, $record->get('title'),
$currentX, $currentY,
'freesans', 'B', self::TITLE_SIZE, 'L',
$usableWidth, self::TITLE_SIZE, true, 0
);
$currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
}
foreach ($record->get('fields') as $field) {
// Write label and value on the same line
// Calculate label width with proportional character spacing
$labelWidth = $pdf->GetStringWidth($field['label'], 'freesans', '', self::LABEL_SIZE);
$charCount = strlen($field['label']);
$spacingPerChar = 0.5;
$totalSpacing = $charCount * $spacingPerChar;
$adjustedWidth = $labelWidth + $totalSpacing;
static::writeText(
$pdf, $field['label'],
$currentX, $currentY,
'freesans', 'B', self::LABEL_SIZE, 'L',
$adjustedWidth, self::LABEL_SIZE, true, 0, $spacingPerChar
);
static::writeText(
$pdf, $field['value'],
$currentX + $adjustedWidth + 2, $currentY,
'freesans', 'B', self::FIELD_SIZE, 'L',
$usableWidth - $adjustedWidth - 2, self::FIELD_SIZE, true, 0, 0.3
);
$currentY += max(self::LABEL_SIZE, self::FIELD_SIZE) + self::FIELD_MARGIN;
}
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$currentX, $barcodeSize + self::BARCODE_MARGIN, $usableWidth - self::TAG_SIZE, self::TAG_SIZE
);
}
}
}

View File

@@ -724,7 +724,7 @@ class License extends Depreciable
public static function getExpiringLicenses($days = 60)
{
return self::whereNull('deleted_at')
return self::whereNull('licenses.deleted_at')
// The termination date is null or within range
->where(function ($query) use ($days) {
@@ -752,7 +752,7 @@ class License extends Depreciable
public function scopeActiveLicenses($query)
{
return $query->whereNull('deleted_at')
return $query->whereNull('licenses.deleted_at')
// The termination date is null or within range
->where(function ($query) {
@@ -768,7 +768,7 @@ class License extends Depreciable
public function scopeExpiredLicenses($query)
{
return $query->whereNull('deleted_at')
return $query->whereNull('licenses.deleted_at')
// The termination date is null or within range
->where(function ($query) {

View File

@@ -7,6 +7,7 @@ use App\Models\Traits\CompanyableChildTrait;
use App\Notifications\CheckinLicenseNotification;
use App\Notifications\CheckoutLicenseNotification;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -64,6 +65,21 @@ class LicenseSeat extends SnipeModel implements ICompanyableChild
return $this->license->getEula();
}
protected function name(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => $this->license->name,
);
}
protected function displayName(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => $this->license->name,
);
}
/**
* Establishes the seat -> license relationship
*

View File

@@ -2,6 +2,7 @@
namespace App\Notifications;
use AllowDynamicProperties;
use App\Helpers\Helper;
use App\Models\Setting;
use Illuminate\Bus\Queueable;
@@ -10,7 +11,7 @@ use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class AcceptanceAssetAcceptedNotification extends Notification
#[AllowDynamicProperties] class AcceptanceAssetAcceptedNotification extends Notification
{
use Queueable;
@@ -22,16 +23,18 @@ class AcceptanceAssetAcceptedNotification extends Notification
public function __construct($params)
{
$this->item_tag = $params['item_tag'];
$this->item_name = $params['item_name'];
$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->accepted_date = Helper::getFormattedDateObject($params['accepted_date'], 'datetime', false);
$this->assigned_to = $params['assigned_to'];
$this->note = $params['note'];
$this->company_name = $params['company_name'];
$this->admin = $params['admin'] ?? null;
$this->settings = Setting::getSettings();
$this->file = $params['file'] ?? null;
$this->qty = $params['qty'] ?? null;
$this->note = $params['note'] ?? null;
$this->admin = $params['admin'] ?? null;
}
@@ -66,6 +69,7 @@ class AcceptanceAssetAcceptedNotification extends Notification
$message = (new MailMessage)->markdown('notifications.markdown.asset-acceptance',
[
'item_tag' => $this->item_tag,
'item_name' => $this->item_name,
'item_model' => $this->item_model,
'item_serial' => $this->item_serial,
'item_status' => $this->item_status,
@@ -73,9 +77,9 @@ class AcceptanceAssetAcceptedNotification extends Notification
'accepted_date' => $this->accepted_date,
'assigned_to' => $this->assigned_to,
'company_name' => $this->company_name,
'admin' => $this->admin,
'qty' => $this->qty,
'intro_text' => trans('mail.acceptance_asset_accepted'),
'admin' => $this->admin,
])
->subject(trans('mail.acceptance_asset_accepted'));

View File

@@ -2,13 +2,14 @@
namespace App\Notifications;
use AllowDynamicProperties;
use App\Helpers\Helper;
use App\Models\Setting;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class AcceptanceAssetAcceptedToUserNotification extends Notification
#[AllowDynamicProperties] class AcceptanceAssetAcceptedToUserNotification extends Notification
{
use Queueable;
@@ -20,16 +21,18 @@ class AcceptanceAssetAcceptedToUserNotification extends Notification
public function __construct($params)
{
$this->item_tag = $params['item_tag'];
$this->item_name = $params['item_name'];
$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->accepted_date = Helper::getFormattedDateObject($params['accepted_date'], 'datetime', false);
$this->assigned_to = $params['assigned_to'];
$this->note = $params['note'];
$this->note = $params['note'] ?? null;
$this->company_name = $params['company_name'];
$this->settings = Setting::getSettings();
$this->file = $params['file'] ?? null;
$this->qty = $params['qty'] ?? null;
$this->admin = $params['admin'] ?? null;
}
/**
@@ -59,6 +62,7 @@ class AcceptanceAssetAcceptedToUserNotification extends Notification
$message = (new MailMessage)->markdown('notifications.markdown.asset-acceptance',
[
'item_tag' => $this->item_tag,
'item_name' => $this->item_name,
'item_model' => $this->item_model,
'item_serial' => $this->item_serial,
'item_status' => $this->item_status,
@@ -66,11 +70,12 @@ class AcceptanceAssetAcceptedToUserNotification extends Notification
'accepted_date' => $this->accepted_date,
'assigned_to' => $this->assigned_to,
'company_name' => $this->company_name,
'admin' => $this->admin,
'qty' => $this->qty,
'intro_text' => trans('mail.acceptance_asset_accepted_to_user', ['site_name' => $this->company_name ?? $this->settings->site_name]),
'intro_text' => trans_choice('mail.acceptance_asset_accepted_to_user', $this->qty, ['qty' => $this->qty, 'site_name' => $this->settings->site_name]),
])
->attach($pdf_path)
->subject(trans('mail.acceptance_asset_accepted_to_user', ['site_name' => $this->settings->site_name]));
->subject(trans_choice('mail.acceptance_asset_accepted_to_user', $this->qty, ['qty' => $this->qty, 'site_name' => $this->settings->site_name]));
return $message;
}

View File

@@ -168,14 +168,14 @@ class AssetObserver
public function saving(Asset $asset)
{
// determine if calculated eol and then calculate it - this should only happen on a new asset
if (is_null($asset->asset_eol_date) && !is_null($asset->purchase_date) && ($asset->model->eol > 0)){
if (is_null($asset->asset_eol_date) && !is_null($asset->purchase_date) && ($asset->model?->eol > 0)) {
$asset->asset_eol_date = $asset->purchase_date->addMonths($asset->model->eol)->format('Y-m-d');
$asset->eol_explicit = false;
}
// determine if explicit and set eol_explicit to true
if (!is_null($asset->asset_eol_date) && !is_null($asset->purchase_date)) {
if($asset->model->eol > 0) {
if ($asset->model?->eol > 0) {
$months = (int) Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date, true);
if($months != $asset->model->eol) {
$asset->eol_explicit = true;
@@ -184,9 +184,9 @@ class AssetObserver
} elseif (!is_null($asset->asset_eol_date) && is_null($asset->purchase_date)) {
$asset->eol_explicit = true;
}
if ((!is_null($asset->asset_eol_date)) && (!is_null($asset->purchase_date)) && (is_null($asset->model->eol) || ($asset->model->eol == 0))) {
if ((!is_null($asset->asset_eol_date)) && (!is_null($asset->purchase_date)) && (is_null($asset->model?->eol) || ($asset->model?->eol == 0))) {
$asset->eol_explicit = true;
}
}
}

View File

@@ -121,6 +121,12 @@ class AccessoryPresenter extends Presenter
'searchable' => true,
'sortable' => true,
'title' => trans('general.unit_cost'),
'class' => 'text-right text-padding-number-cell',
], [
'field' => 'total_cost',
'searchable' => true,
'sortable' => true,
'title' => trans('general.total_cost'),
'footerFormatter' => 'sumFormatterQuantity',
'class' => 'text-right text-padding-number-cell',
], [

View File

@@ -89,17 +89,36 @@ class AssetModelPresenter extends Presenter
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
],
[
'field' => 'assets_assigned_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.assigned'),
'visible' => true,
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
],
[
'field' => 'remaining',
'searchable' => false,
'sortable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.remaining'),
'visible' => true,
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
],
[
'field' => 'assets_archived_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.archived'),
'visible' => true,
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
],
[
'field' => 'depreciation',
'searchable' => false,

View File

@@ -79,6 +79,25 @@ class ComponentPresenter extends Presenter
'title' => trans('general.manufacturer'),
'visible' => false,
'formatter' => 'manufacturersLinkObjFormatter',
], [
'field' => 'location',
'searchable' => true,
'sortable' => true,
'title' => trans('general.location'),
'formatter' => 'locationsLinkObjFormatter',
], [
'field' => 'order_number',
'searchable' => true,
'sortable' => true,
'title' => trans('general.order_number'),
'visible' => true,
], [
'field' => 'purchase_date',
'searchable' => true,
'sortable' => true,
'title' => trans('general.purchase_date'),
'visible' => true,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'min_amt',
'searchable' => false,
@@ -103,33 +122,20 @@ class ComponentPresenter extends Presenter
'visible' => true,
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
], [
'field' => 'location',
'searchable' => true,
'sortable' => true,
'title' => trans('general.location'),
'formatter' => 'locationsLinkObjFormatter',
], [
'field' => 'order_number',
'searchable' => true,
'sortable' => true,
'title' => trans('general.order_number'),
'visible' => true,
], [
'field' => 'purchase_date',
'searchable' => true,
'sortable' => true,
'title' => trans('general.purchase_date'),
'visible' => true,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'purchase_cost',
'searchable' => true,
'sortable' => true,
'title' => trans('general.unit_cost'),
'visible' => true,
'footerFormatter' => 'sumFormatterQuantity',
'class' => 'text-right',
], [
'field' => 'total_cost',
'searchable' => true,
'sortable' => true,
'title' => trans('general.total_cost'),
'footerFormatter' => 'sumFormatterQuantity',
'class' => 'text-right text-padding-number-cell',
], [
'field' => 'notes',
'searchable' => true,

View File

@@ -67,35 +67,6 @@ class ConsumablePresenter extends Presenter
'searchable' => true,
'sortable' => true,
'title' => trans('general.model_no'),
], [
'field' => 'item_no',
'searchable' => true,
'sortable' => true,
'title' => trans('admin/consumables/general.item_no'),
], [
'field' => 'qty',
'searchable' => false,
'sortable' => true,
'title' => trans('admin/components/general.total'),
'visible' => true,
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
], [
'field' => 'remaining',
'searchable' => false,
'sortable' => true,
'title' => trans('admin/components/general.remaining'),
'visible' => true,
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
], [
'field' => 'min_amt',
'searchable' => false,
'sortable' => true,
'title' => trans('general.min_amt'),
'visible' => true,
'formatter' => 'minAmtFormatter',
'class' => 'text-right text-padding-number-cell',
], [
'field' => 'location',
'searchable' => true,
@@ -103,6 +74,12 @@ class ConsumablePresenter extends Presenter
'title' => trans('general.location'),
'formatter' => 'locationsLinkObjFormatter',
], [
'field' => 'item_no',
'searchable' => true,
'sortable' => true,
'title' => trans('admin/consumables/general.item_no'),
], [
'field' => 'manufacturer',
'searchable' => true,
'sortable' => true,
@@ -122,12 +99,42 @@ class ConsumablePresenter extends Presenter
'title' => trans('general.purchase_date'),
'visible' => true,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'min_amt',
'searchable' => false,
'sortable' => true,
'title' => trans('general.min_amt'),
'visible' => true,
'formatter' => 'minAmtFormatter',
'class' => 'text-right text-padding-number-cell',
], [
'field' => 'qty',
'searchable' => false,
'sortable' => true,
'title' => trans('admin/components/general.total'),
'visible' => true,
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
], [
'field' => 'remaining',
'searchable' => false,
'sortable' => true,
'title' => trans('admin/components/general.remaining'),
'visible' => true,
'class' => 'text-right text-padding-number-cell',
'footerFormatter' => 'qtySumFormatter',
], [
'field' => 'purchase_cost',
'searchable' => true,
'sortable' => true,
'title' => trans('general.unit_cost'),
'visible' => true,
'class' => 'text-right text-padding-number-cell',
], [
'field' => 'total_cost',
'searchable' => true,
'sortable' => true,
'title' => trans('general.total_cost'),
'footerFormatter' => 'sumFormatterQuantity',
'class' => 'text-right text-padding-number-cell',
], [

View File

@@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v8.3.1',
'full_app_version' => 'v8.3.1 - build 19577-g7dd493da3',
'build_version' => '19577',
'app_version' => 'v8.3.2',
'full_app_version' => 'v8.3.2 - build 19905-g028b4e7b7',
'build_version' => '19905',
'prerelease_version' => '',
'hash_version' => 'g7dd493da3',
'full_hash' => 'v8.3.1-15-g7dd493da3',
'hash_version' => 'g028b4e7b7',
'full_hash' => 'v8.3.2-319-g028b4e7b7',
'branch' => 'develop',
);

View File

@@ -23,23 +23,39 @@ class CheckoutAcceptanceFactory extends Factory
'assigned_to_id' => User::factory(),
];
}
protected static bool $skipActionLog = false;
public function withoutActionLog(): static
{
// turn off for this create() call
static::$skipActionLog = true;
// ensure it turns back on AFTER creating
return $this->afterCreating(function () {
static::$skipActionLog = false;
});
}
public function configure(): static
{
return $this->afterCreating(function (CheckoutAcceptance $acceptance) {
if (static::$skipActionLog) {
return; // short-circuit
}
if ($acceptance->checkoutable instanceof Asset) {
$this->createdAssociatedActionLogEntry($acceptance);
}
if ($acceptance->checkoutable instanceof Asset && $acceptance->assignedTo instanceof User) {
$acceptance->checkoutable->update([
'assigned_to' => $acceptance->assigned_to_id,
'assigned_type' => get_class($acceptance->assignedTo),
'assigned_to' => $acceptance->assigned_to_id,
'assigned_type'=> get_class($acceptance->assignedTo),
]);
}
});
}
public function forAccessory()
{
return $this->state([

View File

@@ -21,39 +21,45 @@ return new class extends Migration
{
$schema = Schema::connection($this->getConnection());
$schema->create('telescope_entries', function (Blueprint $table) {
$table->bigIncrements('sequence');
$table->uuid('uuid');
$table->uuid('batch_id');
$table->string('family_hash')->nullable();
$table->boolean('should_display_on_index')->default(true);
$table->string('type', 20);
$table->longText('content');
$table->dateTime('created_at')->nullable();
if (! Schema::hasTable('telescope_entries') ) {
$schema->create('telescope_entries', function (Blueprint $table) {
$table->bigIncrements('sequence');
$table->uuid('uuid');
$table->uuid('batch_id');
$table->string('family_hash')->nullable();
$table->boolean('should_display_on_index')->default(true);
$table->string('type', 20);
$table->longText('content');
$table->dateTime('created_at')->nullable();
$table->unique('uuid');
$table->index('batch_id');
$table->index('family_hash');
$table->index('created_at');
$table->index(['type', 'should_display_on_index']);
});
$table->unique('uuid');
$table->index('batch_id');
$table->index('family_hash');
$table->index('created_at');
$table->index(['type', 'should_display_on_index']);
});
}
$schema->create('telescope_entries_tags', function (Blueprint $table) {
$table->uuid('entry_uuid');
$table->string('tag');
if (! Schema::hasTable('telescope_entries_tags') ) {
$schema->create('telescope_entries_tags', function (Blueprint $table) {
$table->uuid('entry_uuid');
$table->string('tag');
$table->primary(['entry_uuid', 'tag']);
$table->index('tag');
$table->primary(['entry_uuid', 'tag']);
$table->index('tag');
$table->foreign('entry_uuid')
->references('uuid')
->on('telescope_entries')
->onDelete('cascade');
});
$table->foreign('entry_uuid')
->references('uuid')
->on('telescope_entries')
->onDelete('cascade');
});
}
$schema->create('telescope_monitoring', function (Blueprint $table) {
$table->string('tag')->primary();
});
if (! Schema::hasTable('telescope_monitoring') ) {
$schema->create('telescope_monitoring', function (Blueprint $table) {
$table->string('tag')->primary();
});
}
}
/**

97
package-lock.json generated
View File

@@ -20,6 +20,8 @@
"chart.js": "^2.9.4",
"clipboard": "^2.0.11",
"css-loader": "^5.0.0",
"dompurify": "^3.2.7",
"easymde": "^2.20.0",
"ekko-lightbox": "^5.1.1",
"imagemin": "^9.0.1",
"jquery-slimscroll": "^1.3.8",
@@ -2039,6 +2041,15 @@
"source-map": "^0.6.0"
}
},
"node_modules/@types/codemirror": {
"version": "5.60.16",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.16.tgz",
"integrity": "sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw==",
"license": "MIT",
"dependencies": {
"@types/tern": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"dev": true,
@@ -2168,6 +2179,12 @@
"version": "7.0.15",
"license": "MIT"
},
"node_modules/@types/marked": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz",
"integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==",
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"dev": true,
@@ -2258,6 +2275,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/ws": {
"version": "8.5.10",
"dev": true,
@@ -3750,6 +3783,21 @@
"node": ">=6"
}
},
"node_modules/codemirror": {
"version": "5.65.20",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.20.tgz",
"integrity": "sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA==",
"license": "MIT"
},
"node_modules/codemirror-spell-checker": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz",
"integrity": "sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==",
"license": "MIT",
"dependencies": {
"typo-js": "*"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"license": "Apache-2.0",
@@ -4617,10 +4665,13 @@
}
},
"node_modules/dompurify": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
"integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
"optional": true
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "2.8.0",
@@ -4716,6 +4767,19 @@
"readable-stream": "^2.0.2"
}
},
"node_modules/easymde": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz",
"integrity": "sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==",
"license": "MIT",
"dependencies": {
"@types/codemirror": "^5.60.10",
"@types/marked": "^4.0.7",
"codemirror": "^5.65.15",
"codemirror-spell-checker": "1.1.2",
"marked": "^4.1.0"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -6803,6 +6867,13 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/jspdf/node_modules/dompurify": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
"integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true
},
"node_modules/junk": {
"version": "3.1.0",
"dev": true,
@@ -7367,6 +7438,18 @@
"semver": "bin/semver.js"
}
},
"node_modules/marked": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -10380,6 +10463,12 @@
"version": "0.0.6",
"license": "MIT"
},
"node_modules/typo-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.3.1.tgz",
"integrity": "sha512-elJkpCL6Z77Ghw0Lv0lGnhBAjSTOQ5FhiVOCfOuxhaoTT2xtLVbqikYItK5HHchzPbHEUFAcjOH669T2ZzeCbg==",
"license": "BSD-3-Clause"
},
"node_modules/uint8array-extras": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz",

View File

@@ -40,6 +40,8 @@
"chart.js": "^2.9.4",
"clipboard": "^2.0.11",
"css-loader": "^5.0.0",
"dompurify": "^3.2.7",
"easymde": "^2.20.0",
"ekko-lightbox": "^5.1.1",
"imagemin": "^9.0.1",
"jquery-slimscroll": "^1.3.8",

View File

@@ -1501,6 +1501,9 @@ caption.tableCaption {
white-space: preserve;
display: inline-block;
}
input[name="columnsSearch"] {
width: 120px;
}
/*# sourceMappingURL=app.css.map*/

File diff suppressed because one or more lines are too long

View File

@@ -1125,6 +1125,9 @@ caption.tableCaption {
white-space: preserve;
display: inline-block;
}
input[name="columnsSearch"] {
width: 120px;
}
/*# sourceMappingURL=overrides.css.map*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20716
public/js/dist/all.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,9 @@
{
"/js/dist/all.js": "/js/dist/all.js?id=76d88f0f91b852f7eecbce357ab5858b",
"/js/dist/all.js": "/js/dist/all.js?id=7c3f9e8eb2f336d70103b33d82976c96",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=42f97cd5b9ee7521b04a448e7fc16ac9",
"/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=d81a7ed323f68a7c5e3e9115f7fb5404",
"/css/build/overrides.css": "/css/build/overrides.css?id=d8bef2b8ef03ee8dbb120749211eafc0",
"/css/build/app.css": "/css/build/app.css?id=1bf6a5e78cbccff6e6d32640c28c54b8",
"/css/build/overrides.css": "/css/build/overrides.css?id=d6ec1f1e36c57f8cd96218d2a59a2580",
"/css/build/app.css": "/css/build/app.css?id=d591faf82795dc8151a6d3f84c26f5a4",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=ee0ed88465dd878588ed044eefb67723",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=3d8a3d2035ea28aaad4a703c2646f515",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=3979929a3423ff35b96b1fc84299fdf3",
@@ -19,7 +19,7 @@
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=b2cd9f59d7e8587939ce27b2d3363d82",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=7277edd636cf46aa7786a4449ce0ead7",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=cbd06cc1d58197ccc81d4376bbaf0d28",
"/css/dist/all.css": "/css/dist/all.css?id=dd5f7ab27ec80569b90d63a883718ff9",
"/css/dist/all.css": "/css/dist/all.css?id=b83f91cf2fc0fe3aae187b7e11c43cf7",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",

View File

@@ -22,6 +22,8 @@ require('jquery.iframe-transport'); //probably not needed anymore, if I'm honest
require('blueimp-file-upload')
require('bootstrap-colorpicker')
require('bootstrap-datepicker')
window.EasyMDE = require('easymde');
window.DOMPurify = require('dompurify');
require('ekko-lightbox') //TODO - this doesn't seem jquery-ish, we might need to do something weird here
// it *does* require Bootstrap, which requires jquery, so maybe that's OK
// it seems to work...

View File

@@ -1260,4 +1260,8 @@ caption.tableCaption {
clip: rect(0,0,0,0);
white-space: preserve;
display: inline-block;
}
input[name="columnsSearch"] {
width: 120px;
}

View File

@@ -309,6 +309,7 @@ return [
'total_licenses' => 'total licenses',
'total_accessories' => 'total accessories',
'total_consumables' => 'total consumables',
'total_cost' => 'Total Cost',
'type' => 'Type',
'undeployable' => 'Un-deployable',
'unknown_admin' => 'Unknown Admin',
@@ -518,7 +519,10 @@ return [
'item_notes' => ':item Notes',
'item_name_var' => ':item Name',
'error_user_company' => 'Checkout target company and asset company do not match',
'error_user_company_multiple' => 'One or more of the checkout target company and asset company do not match',
'error_user_company_accept_view' => 'An Asset assigned to you belongs to a different company so you can\'t accept nor deny it, please check with your manager',
'error_assets_already_checked_out' => 'One or more of the assets are already checked out',
'assigned_assets_removed' => 'The following were removed from the selected assets because they are already checked out',
'importer' => [
'checked_out_to_fullname' => 'Checked Out to: Full Name',
'checked_out_to_first_name' => 'Checked Out to: First Name',

View File

@@ -31,7 +31,7 @@ return [
'Low_Inventory_Report' => 'Low Inventory Report',
'a_user_canceled' => 'A user has canceled an item request on the website',
'a_user_requested' => 'A user has requested an item on the website',
'acceptance_asset_accepted_to_user' => 'You have accepted an item assigned to you by :site_name',
'acceptance_asset_accepted_to_user' => 'You have accepted an item assigned to you by :site_name|You have accepted :qty items assigned to you by :site_name',
'acceptance_asset_accepted' => 'A user has accepted an item',
'acceptance_asset_declined' => 'A user has declined an item',
'send_pdf_copy' => 'Send a copy of this acceptance to my email address',

View File

@@ -244,6 +244,18 @@
</div>
</div>
@endif
@if ($accessory->purchase_cost)
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">
<strong>
{{ trans('general.total_cost') }}
</strong>
</div>
<div class="col-md-9" style="word-wrap: break-word;">
{{ Helper::formatCurrencyOutput($accessory->totalCostSum()) }}
</div>
</div>
@endif
<div class="row">
<div class="col-md-3" style="padding-bottom: 10px;">

View File

@@ -20,11 +20,21 @@
}
.m-signature-pad--body {
border-style: solid;
border-style: dashed;
border-color: grey;
border-width: thin;
border-width: thick;
padding-top: 0px;
}
.m-signature-pad {
box-shadow: none;
background-color: inherit;
border: none;
}
</style>
@@ -90,30 +100,37 @@
<canvas style="width:100%;"></canvas>
<input type="hidden" name="signature_output" id="signature_output">
</div>
<div class="col-md-12 col-sm-12 col-lg-12 col-xs-12 text-center">
<div class="col-md-12 col-sm-12 col-lg-12 col-xs-12 text-left">
<button type="button" class="btn btn-sm btn-default clear" data-action="clear" id="clear_button">{{trans('general.clear_signature')}}</button>
</div>
</div>
</div>
@endif
@if (auth()->user()->email!='')
<div class="col-md-12" style="padding-top: 20px; display: none;" id="showEmailBox">
<label class="form-control">
<input type="checkbox" value="1" name="send_copy" id="send_copy" checked="checked" aria-label="send_copy">
{{ trans('mail.send_pdf_copy') }} ({{ auth()->user()->email }})
</label>
</div>
@endif
</div> <!-- / box-body -->
<div class="box-footer text-right" style="display: none;" id="showSubmit">
<button type="submit" class="btn btn-success" id="submit-button">
<i class="fa fa-check icon-white" aria-hidden="true" id="submitIcon"></i>
<span id="buttonText">
<div class="box-footer" style="display: none;" id="showSubmit">
<div class="row">
<div class="col-md-7">
@if (auth()->user()->email!='')
<div class="col-md-12" style="display: none;" id="showEmailBox">
<label class="form-control">
<input type="checkbox" value="1" name="send_copy" id="send_copy" checked="checked" aria-label="send_copy">
{{ trans('mail.send_pdf_copy') }} ({{ auth()->user()->email }})
</label>
</div>
@endif
</div>
<div class="col-md-5 text-right">
<button type="submit" class="btn btn-success" id="submit-button">
<i class="fa fa-check icon-white" aria-hidden="true" id="submitIcon"></i>
<span id="buttonText">
{{ trans_choice('general.i_accept_item', $acceptance->qty ?? null) }}
</span>
</button>
</button>
</div>
</div>
</div><!-- /.box-footer -->
</div> <!-- / box-default -->
</div> <!-- / col -->

View File

@@ -61,6 +61,7 @@
@if ($category->category_type=='asset')
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="categoryAssetsTable"
id="categoryAssetsTable"
data-buttons="assetButtons"

View File

@@ -94,6 +94,7 @@
data-cookie-id-table="assetsListingTable"
data-id-table="assetsListingTable"
data-side-pagination="server"
data-show-columns-search="true"
data-sort-order="asc"
data-toolbar="#assetsBulkEditToolbar"
data-bulk-button-id="#bulkAssetEditButton"

View File

@@ -207,6 +207,13 @@
{{ Helper::formatCurrencyOutput($component->purchase_cost) }} </div>
@endif
@if ($component->purchase_cost)
<div class="col-md-12" style="padding-bottom: 5px;"><strong>{{ trans('general.total_cost') }}:</strong>
{{ $snipeSettings->default_currency }}
{{ Helper::formatCurrencyOutput($component->totalCostSum()) }} </div>
@endif
@if ($component->order_number)
<div class="col-md-12" style="padding-bottom: 5px;"><strong>{{ trans('general.order_number') }}:</strong>
{{ $component->order_number }} </div>

View File

@@ -281,6 +281,18 @@
</div>
@endif
@if ($consumable->purchase_cost)
<div class="row">
<div class="col-md-3">
{{ trans('general.total_cost') }}
</div>
<div class="col-md-9">
{{ $snipeSettings->default_currency }}
{{ Helper::formatCurrencyOutput($consumable->totalCostSum()) }}
</div>
</div>
@endif
@if ($consumable->order_number)
<div class="row">
<div class="col-md-3">

View File

@@ -65,6 +65,7 @@
<table
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="depreciationsAssetTable"
data-id-table="depreciationsAssetTable"
id="depreciationsAssetTable"

View File

@@ -27,6 +27,26 @@
<form class="form-horizontal" method="post" action="" autocomplete="off">
{{ csrf_field() }}
@if ($removed_assets->isNotEmpty())
<div class="box box-solid box-warning">
<div class="box-header with-border">
<span class="box-title col-xs-12">Warning</span>
</div>
<div class="box-body">
<p>{{ trans('general.assigned_assets_removed') }}</p>
<ul>
@foreach($removed_assets as $removed_asset)
<li>
<a href="{{ route('hardware.show', $removed_asset->id) }}">
{{ $removed_asset->present()->fullName }}
</a>
</li>
@endforeach
</ul>
</div>
</div>
@endif
@include ('partials.forms.edit.asset-select', [
'translated_name' => trans('general.assets'),
'fieldname' => 'selected_assets[]',
@@ -147,7 +167,6 @@
return true; // ensure form still submits
});
$('#assigned_assets_select').select2('open');
setTimeout(function () {
const $searchField = $('.select2-search__field');
const $results = $('.select2-results');

View File

@@ -49,6 +49,7 @@
<table
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="dueAssetcheckinListing"
data-id-table="dueAssetcheckinListing"
data-side-pagination="server"

View File

@@ -94,7 +94,7 @@
</div>
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user', 'style' => session('checkout_to_type') == 'user' ? '' : 'display: none;'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user', 'style' => (session('checkout_to_type') ?: 'user') == 'user' ? '' : 'display: none;'])
<!-- We have to pass unselect here so that we don't default to the asset that's being checked out. We want that asset to be pre-selected everywhere else. -->
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.select_asset'), 'fieldname' => 'assigned_asset', 'company_id' => $asset->company_id, 'unselect' => 'true', 'style' => session('checkout_to_type') == 'asset' ? '' : 'display: none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => session('checkout_to_type') == 'location' ? '' : 'display: none;'])

View File

@@ -47,6 +47,8 @@
{{-- Page content --}}
@section('content')
<div class="row">
<div class="col-md-12">
<div class="box">
@@ -66,6 +68,7 @@
data-show-footer="true"
data-sort-order="asc"
data-sort-name="name"
data-show-columns-search="true"
data-toolbar="#assetsBulkEditToolbar"
data-bulk-button-id="#bulkAssetEditButton"
data-bulk-form-id="#assetsBulkForm"

View File

@@ -1285,6 +1285,7 @@
<table
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="assetsTable"
data-id-table="assetsTable"
data-side-pagination="server"

View File

@@ -1086,6 +1086,66 @@ dir="{{ Helper::determineLanguageDirection() }}">
<script nonce="{{ csrf_token() }}">
const easymde = new EasyMDE({
element: document.getElementById('markdown-textarea'),
toolbar: [
"bold",
"italic",
"strikethrough",
"quote",
"code",
"|",
"link",
"|",
"heading-1",
"heading-2",
"heading-3",
"|",
"unordered-list",
"ordered-list",
"|",
"horizontal-rule",
"|",
"preview",
"side-by-side",
"|",
"undo",
"redo",
"|",
"guide"
],
status: [], // Optional usage
promptURLs: true,
renderingConfig: {
singleLineBreaks: false,
codeSyntaxHighlighting: true,
sanitizerFunction: (renderedHTML) => {
// Using DOMPurify and only allowing <b> tags
return DOMPurify.sanitize(renderedHTML, {ALLOWED_TAGS: [
'b',
'strong',
'h1',
'h2',
'h3',
'code',
'blockquote',
'i',
'strikethrough',
'del',
's',
'q',
'p',
'br',
'em',
'a'
]})
},
},
showIcons: ["code", "table"],
spellChecker: false,
sideBySideFullscreen: false,
});
$.fn.datepicker.dates['{{ app()->getLocale() }}'] = {
days: [
"{{ trans('datepicker.days.sunday') }}",

View File

@@ -210,6 +210,7 @@
@include('partials.asset-bulk-actions')
<table
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="assetsListingTable"
data-id-table="assetsListingTable"
data-side-pagination="server"
@@ -237,6 +238,7 @@
<table
role="table"
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="assetsAssignedListingTable"
data-id-table="assetsAssignedListingTable"
data-side-pagination="server"
@@ -262,6 +264,7 @@
<table
role="table"
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="RTDassetsListingTable"
data-id-table="RTDassetsListingTable"
data-side-pagination="server"

View File

@@ -20,6 +20,11 @@
{{-- Page content --}}
@section('content')
<style>
.editor-toolbar {
padding-top: 0px;
}
</style>
<div class="row">
<div class="col-md-9">
@if ($item->id)
@@ -178,7 +183,10 @@
<div class="form-group {{ $errors->has('notes') ? ' has-error' : '' }}">
<label for="notes" class="col-md-3 control-label">{{ trans('admin/maintenances/form.notes') }}</label>
<div class="col-md-7">
<textarea class="col-md-6 form-control" id="notes" name="notes">{{ old('notes', $item->notes) }}</textarea>
<textarea class="col-md-6 form-control" rows="4" name="notes" id="markdown-textarea">{{ old('notes', $item->notes) }}</textarea>
<p class="help-block">
{!! trans('general.markdown') !!}</p>
{!! $errors->first('notes', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>

View File

@@ -173,7 +173,7 @@ use Carbon\Carbon;
{{ trans('admin/maintenances/form.notes') }}
</div>
<div class="col-md-9">
{!! nl2br(Helper::parseEscapedMarkedownInline($maintenance->notes)) !!}
{!! Helper::parseEscapedMarkedown($maintenance->notes) !!}
</div>
</div> <!-- /row -->
@endif
@@ -200,21 +200,6 @@ use Carbon\Carbon;
</div>
@endif
<div class="col-md-12">
<ul class="list-unstyled" style="line-height: 22px; padding-bottom: 20px;">
@if ($maintenance->notes)
<li>
<strong>{{ trans('general.notes') }}</strong>:
{!! nl2br(Helper::parseEscapedMarkedownInline($maintenance->notes)) !!}
</li>
@endif
</ul>
</div>
@can('update', $maintenance)
<div class="col-md-12">
<a href="{{ route('maintenances.edit', [$maintenance->id]) }}" style="width: 100%;" class="btn btn-sm btn-warning btn-social">

View File

@@ -104,6 +104,7 @@
<div class="table table-responsive">
<table
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="assetsListingTable"
data-id-table="assetsListingTable"
data-toolbar="#assetsBulkEditToolbar"

View File

@@ -5,7 +5,7 @@
@endphp
@foreach($models as $model)
@if ($model->fieldset ? $model->fieldset->count() > 0 : false)
@if (($model) && ($model->fieldset ? $model->fieldset->count() > 0 : false))
@php
$anyModelHasCustomFields++;
@endphp

View File

@@ -87,9 +87,11 @@
<table
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="assetListingTable"
data-id-table="assetListingTable"
data-side-pagination="server"
data-show-footer="true"
data-toolbar="#assetsBulkEditToolbar"
data-bulk-button-id="#bulkAssetEditButton"
data-bulk-form-id="#assetsBulkForm"

View File

@@ -6,6 +6,9 @@
@component('mail::table')
| | |
| ------------- | ------------- |
@if (isset($item_name))
| **{{ trans('general.name') }}** | {{ $item_name }} |
@endif
| **{{ trans('mail.user') }}** | {{ $assigned_to }} |
@if (isset($user->location))
| **{{ trans('general.location') }}** | {{ $user->location->name }} |

View File

@@ -4,22 +4,25 @@
<div class="btn-group" data-toggle="buttons">
@if ((isset($user_select)) && ($user_select!='false'))
<label class="btn btn-default{{ session('checkout_to_type') == 'user' ? ' active' : '' }}">
<input name="checkout_to_type" value="user" aria-label="checkout_to_type" type="radio" checked="checked">
<label class="btn btn-default{{ (session('checkout_to_type') ?: 'user') == 'user' ? ' active' : '' }}">
<input name="checkout_to_type" value="user" aria-label="checkout_to_type"
type="radio" {{ (session('checkout_to_type') ?: 'user') == 'user' ? 'checked' : '' }}>
<x-icon type="user" />
{{ trans('general.user') }}
</label>
@endif
@if ((isset($asset_select)) && ($asset_select!='false'))
<label class="btn btn-default{{ session('checkout_to_type') == 'asset' ? ' active' : '' }}">
<input name="checkout_to_type" value="asset" aria-label="checkout_to_type" type="radio">
<label class="btn btn-default{{ session('checkout_to_type') == 'asset' ? ' active' : '' }}">
<input name="checkout_to_type" value="asset" aria-label="checkout_to_type"
type="radio" {{ session('checkout_to_type') == 'asset' ? 'checked': '' }}>
<i class="fas fa-barcode" aria-hidden="true"></i>
{{ trans('general.asset') }}
</label>
@endif
@if ((isset($location_select)) && ($location_select!='false'))
<label class="btn btn-default{{ session('checkout_to_type') == 'location' ? ' active' : '' }}">
<input name="checkout_to_type" value="location" aria-label="checkout_to_type" class="active" type="radio">
<label class="btn btn-default{{ session('checkout_to_type') == 'location' ? ' active' : '' }}">
<input name="checkout_to_type" value="location" aria-label="checkout_to_type"
type="radio" {{ session('checkout_to_type') == 'location' ? 'checked' : '' }}>
<i class="fas fa-map-marker-alt" aria-hidden="true"></i>
{{ trans('general.location') }}
</label>

View File

@@ -426,7 +426,25 @@
</div>
<!-- Created Date -->
<!-- Purchase Cost -->
<div class="form-group purchase-range{{ ($errors->has('purchase_cost_start') || $errors->has('purchase_cost_end')) ? ' has-error' : '' }}">
<label for="purchase_cost_start" class="col-md-3 control-label">{{ trans('admin/hardware/form.cost') }}</label>
<div class="input-group col-md-7">
<input type="number" min="0" step="0.01" class="form-control" name="purchase_cost_start" aria-label="purchase_cost_start" value="{{ $template->textValue('purchase_cost_start', old('purchase_cost_start')) }}">
<span class="input-group-addon">{{ strtolower(trans('general.to')) }}</span>
<input type="number" min="0" step="0.01" class="form-control" name="purchase_cost_end" aria-label="purchase_cost_end" value="{{ $template->textValue('purchase_cost_end', old('purchase_cost_end')) }}">
</div>
@if ($errors->has('purchase_cost_start') || $errors->has('purchase_cost_end'))
<div class="col-md-9 col-lg-offset-3">
{!! $errors->first('purchase_cost_start', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
{!! $errors->first('purchase_cost_end', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
@endif
</div>
<!-- Created Date -->
<div class="form-group created-range{{ ($errors->has('created_start') || $errors->has('created_end')) ? ' has-error' : '' }}">
<label for="created_start" class="col-md-3 control-label">{{ trans('general.created_at') }} </label>
<div class="input-daterange input-group col-md-7" id="created-range-datepicker">

View File

@@ -150,7 +150,7 @@
<div class="box-body">
<p>
{!! trans('admin/settings/general.backups_path', ['path'=> 'storage/app/backup']) !!}
{!! trans('admin/settings/general.backups_path', ['path'=> 'storage/app/backups']) !!}
</p>
@if (config('app.lock_passwords')===true)

View File

@@ -24,6 +24,7 @@
data-bulk-button-id="#bulkAssetEditButton"
data-bulk-form-id="#assetsBulkForm"
id="assetsListingTable"
data-show-columns-search="true"
data-buttons="assetButtons"
class="table table-striped snipe-table"
data-url="{{route('api.assets.index', ['status_id' => $statuslabel->id]) }}"

View File

@@ -114,6 +114,7 @@
<table
data-cookie-id-table="suppliersAssetsTable"
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-id-table="suppliersAssetsTable"
data-show-footer="true"
data-side-pagination="server"

View File

@@ -94,6 +94,17 @@
</a>
</li>
<li>
<a href="#eulas" data-toggle="tab">
<span class="hidden-lg hidden-md" aria-hidden="true">
<x-icon type="files" class="fa-2x" />
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.eula') }}
{!! ($user->eulas->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($user->eulas->count()).'</span>' : '' !!}
</span>
</a>
</li>
<li>
<a href="#history" data-toggle="tab">
<span class="hidden-lg hidden-md">
@@ -825,6 +836,7 @@
<table
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-show-columns-search="true"
data-cookie-id-table="userAssetsListingTable"
data-id-table="userAssetsListingTable"
data-side-pagination="server"
@@ -1001,6 +1013,35 @@
</div> <!--/ROW-->
</div><!--/FILES-->
<div class="tab-pane" id="eulas">
<table
data-toolbar="#userEULAToolbar"
data-cookie-id-table="userEULATable"
data-id-table="userEULATable"
id="userEULATable"
data-side-pagination="client"
data-show-footer="true"
data-show-refresh="false"
data-sort-order="asc"
data-sort-name="name"
class="table table-striped snipe-table table-hover"
data-url="{{ route('api.user.eulas', $user) }}"
data-export-options='{
"fileName": "export-eula-{{ str_slug($user->username) }}-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","delete","purchasecost", "icon"]
}'>
<thead>
<tr>
<th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th>
<th data-visible="true" data-field="item.name">{{ trans('general.item') }}</th>
<th data-visible="true" data-field="created_at" data-sortable="true" data-formatter="dateDisplayFormatter">{{ trans('general.accepted_date') }}</th>
<th data-field="note">{{ trans('general.notes') }}</th>
<th data-field="url" data-formatter="fileDownloadButtonsFormatter">{{ trans('general.download') }}</th>
</tr>
</thead>
</table>
</div><!-- /eulas-tab -->
<div class="tab-pane" id="history">
<div class="table-responsive">

View File

@@ -571,6 +571,13 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
'assignedAccessories'
]
)->name('api.assets.assigned_accessories');
Route::get('{asset}/assigned/components',
[
Api\AssetsController::class,
'assignedComponents'
]
)->name('api.assets.assigned_components');
/** End assigned routes */
});
@@ -583,9 +590,7 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
// the model name to be the parameter - and i think it's a good differentiation in the code while we convert the others.
Route::patch('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.update');
Route::put('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.put-update');
Route::put('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.put-update');
Route::resource('hardware',
Api\AssetsController::class,
['names' => [

View File

@@ -725,7 +725,7 @@ Route::group(['middleware' => 'web'], function () {
'destroy'
]
)->name('ui.files.destroy')
->where(['object_type' => 'assets|hardware|models|users|locations|accessories|consumables|licenses|components']);
->where(['object_type' => 'assets|maintenances|hardware|models|users|locations|accessories|consumables|licenses|components']);
});

View File

@@ -0,0 +1,79 @@
<?php
namespace Tests\Feature\Assets\Api;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Component;
use App\Models\User;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\TestCase;
class AssignedComponentsTest extends TestCase
{
public function test_requires_permission()
{
$this->actingAsForApi(User::factory()->create())
->getJson(route('api.assets.assigned_components', Asset::factory()->create()))
->assertForbidden();
}
public function test_adheres_to_company_scoping()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$asset = Asset::factory()->for($companyA)->create();
$user = User::factory()->for($companyB)->viewAssets()->create();
$this->actingAsForApi($user)
->getJson(route('api.assets.assigned_components', $asset))
->assertOk()
->assertStatusMessageIs('error')
->assertMessagesAre('Asset not found');
}
public function test_can_get_components_assigned_to_specific_asset()
{
$unassociatedComponent = Component::factory()->create();
$asset = Asset::factory()->hasComponents(2)->create();
$componentsAssignedToAsset = $asset->components;
$this->actingAsForApi(User::factory()->viewAssets()->create())
->getJson(route('api.assets.assigned_components', $asset))
->assertOk()
->assertResponseContainsInRows($componentsAssignedToAsset)
->assertResponseDoesNotContainInRows($unassociatedComponent)
->assertJson(function (AssertableJson $json) {
$json->where('total', 2)
->count('rows', 2)
->etc();
});
}
public function test_adheres_to_offset_and_limit()
{
$asset = Asset::factory()->hasComponents(2)->create();
$componentsAssignedToAsset = $asset->components;
$this->actingAsForApi(User::factory()->viewAssets()->create())
->getJson(route('api.assets.assigned_components', [
'asset' => $asset,
'offset' => 1,
'limit' => 1,
]))
->assertOk()
->assertResponseDoesNotContainInRows($componentsAssignedToAsset->first())
->assertResponseContainsInRows($componentsAssignedToAsset->last())
->assertJson(function (AssertableJson $json) {
$json->where('total', 2)
->count('rows', 1)
->etc();
});
}
}

View File

@@ -29,6 +29,25 @@ class BulkEditAssetsTest extends TestCase
])->assertStatus(200);
}
public function test_handles_model_being_deleted()
{
$this->withoutExceptionHandling();
$user = User::factory()->viewAssets()->editAssets()->create();
$assets = Asset::factory()->count(2)->create();
$assets->first()->model->forceDelete();
$id_array = $assets->pluck('id')->toArray();
$this->actingAs($user)->post('/hardware/bulkedit', [
'ids' => $id_array,
'order' => 'asc',
'bulk_actions' => 'edit',
'sort' => 'id'
])->assertStatus(200);
}
public function test_standard_user_cannot_access_page()
{
$user = User::factory()->create();

View File

@@ -125,4 +125,32 @@ class EditAssetTest extends TestCase
$this->assertEquals($currentLocation->id, $asset->location_id);
}
public function test_handles_model_being_deleted()
{
$this->withoutExceptionHandling();
$newStatus = StatusLabel::factory()->create();
$asset = Asset::factory()->create();
$asset->model()->forceDelete();
$this->actingAs(User::factory()->viewAssets()->editAssets()->create())
->from(route('hardware.edit', $asset))
->put(route('hardware.update', $asset), [
'redirect_option' => 'index',
'purchase_date' => '2025-08-30',
'name' => 'New name',
'asset_tags' => 'New Asset Tag',
'status_id' => $newStatus->id,
// triggers potential issue in AssetObserver's saving method
'model_id' => AssetModel::factory()->create()->id,
]);
$this->assertDatabaseHas('assets', [
'id' => $asset->id,
'status_id' => $newStatus->id,
]);
}
}

View File

@@ -4,8 +4,11 @@ namespace Tests\Feature\Checkouts\Ui;
use App\Mail\CheckoutAssetMail;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Location;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\ExpectationFailedException;
use Tests\TestCase;
@@ -89,4 +92,99 @@ class BulkAssetCheckoutTest extends TestCase
$this->fail('Asset checkout email was sent when the entire checkout failed.');
}
}
public static function checkoutTargets()
{
yield 'Checkout to user' => [
function () {
return [
'type' => 'user',
'target' => User::factory()->forCompany()->create(),
];
}
];
yield 'Checkout to asset' => [
function () {
return [
'type' => 'asset',
'target' => Asset::factory()->forCompany()->create(),
];
}
];
yield 'Checkout to location' => [
function () {
return [
'type' => 'location',
'target' => Location::factory()->forCompany()->create(),
];
}
];
}
#[DataProvider('checkoutTargets')]
public function test_adheres_to_full_multiple_company_support($data)
{
['type' => $type, 'target' => $target] = $data();
$this->settings->enableMultipleFullCompanySupport();
// create two companies
[$companyA, $companyB] = Company::factory()->count(2)->create();
// create an asset for each company
$assetForCompanyA = Asset::factory()->for($companyA)->create();
$assetForCompanyB = Asset::factory()->for($companyB)->create();
$this->assertNull($assetForCompanyA->assigned_to, 'Asset should not be assigned before attempting this test case.');
$this->assertNull($assetForCompanyB->assigned_to, 'Asset should not be assigned before attempting this test case.');
// attempt to bulk checkout both items to the target
$response = $this->actingAs(User::factory()->superuser()->create())
->post(route('hardware.bulkcheckout.store'), [
'selected_assets' => [
$assetForCompanyA->id,
$assetForCompanyB->id,
],
'checkout_to_type' => $type,
"assigned_$type" => $target->id,
]);
// ensure bulk checkout is blocked
$this->assertNull($assetForCompanyA->fresh()->assigned_to, 'Asset was checked out across companies.');
$this->assertNull($assetForCompanyB->fresh()->assigned_to, 'Asset was checked out across companies.');
// ensure redirected back
$response->assertRedirectToRoute('hardware.bulkcheckout.show');
}
#[DataProvider('checkoutTargets')]
public function test_prevents_checkouts_of_checked_out_items($data)
{
['type' => $type, 'target' => $target] = $data();
$asset = Asset::factory()->create();
$checkedOutAsset = Asset::factory()->assignedToUser()->create();
$existingUserId = $checkedOutAsset->assigned_to;
$response = $this->actingAs(User::factory()->superuser()->create())
->post(route('hardware.bulkcheckout.store'), [
'selected_assets' => [
$asset->id,
$checkedOutAsset->id,
],
'checkout_to_type' => $type,
"assigned_$type" => $target->id,
]);
$this->assertEquals(
$existingUserId,
$checkedOutAsset->fresh()->assigned_to,
'Asset was checked out when it should have been prevented.'
);
// ensure redirected back
$response->assertRedirectToRoute('hardware.bulkcheckout.show');
}
}

View File

@@ -57,13 +57,10 @@ class ExpiringAlertsNotificationTest extends TestCase
$this->artisan('snipeit:expiring-alerts')->assertExitCode(0);
Mail::assertSent(ExpiringAssetsMail::class, function($mail) use ($alert_email, $expiringWarrantyAsset) {
return $mail->hasTo($alert_email) && $mail->assets->contains($expiringWarrantyAsset);
});
Mail::assertSent(ExpiringAssetsMail::class, function($mail) use ($alert_email, $expiringEOLAsset) {
return $mail->hasTo($alert_email) && $mail->assets->contains($expiringEOLAsset);
Mail::assertSent(ExpiringAssetsMail::class, function($mail) use ($alert_email, $expiringWarrantyAsset, $expiringEOLAsset) {
return $mail->hasTo($alert_email) && ($mail->assets->contains($expiringEOLAsset) || $mail->assets->contains($expiringWarrantyAsset));
});
Mail::assertNotSent(ExpiringAssetsMail::class, function($mail) use ($alert_email, $notExpiringAsset, $alreadyExpiredAsset) {
return $mail->assets->contains($alert_email) || ($mail->assets->contains($alreadyExpiredAsset) && ($mail->assets->contains($notExpiringAsset)));

View File

@@ -62,6 +62,7 @@ if ($argc > 1){
break;
case '--no-interactive':
$no_interactive = true;
putenv("COMPOSER_NO_INTERACTION=1"); //put composer in non-interactive mode aswell
break;
default: // for legacy support from before we started using --branch
$branch = $argv[$arg];
@@ -443,7 +444,8 @@ if ((strpos('git version', $git_version)) === false) {
echo $git_fetch;
echo '-- '.$git_stash;
echo '-- '.$git_checkout;
echo '-- '.$git_pull."\n";
echo '-- '.$git_pull;
echo "\n";
} else {
echo "Git is NOT installed. You can still use this upgrade script to run common \n";
echo "migration commands, but you will have to manually download the updated files. \n\n";
@@ -539,7 +541,7 @@ echo "--------------------------------------------------------\e[39m\n\n";
exec('php artisan down', $down_results, $return_code);
echo '-- ' . implode("\n", $down_results) . "\n";
if ($return_code > 0) {
die("Something went wrong with downing your site. This can't be good. Please investigate the error. Aborting!\n\n");
die("Something went wrong with downing your site. This can't be good. Please investigate the error and be sure to check https://snipe-it.readme.io/docs/common-issues and https://snipe-it.readme.io/docs/installation-issues for solutions to common upgrading issues. Aborting!\n\n");
}
unset($return_code);

View File

@@ -24,6 +24,7 @@ mix
"./node_modules/blueimp-file-upload/css/jquery.fileupload-ui.css",
"./node_modules/ekko-lightbox/dist/ekko-lightbox.css",
"./node_modules/bootstrap-table/dist/bootstrap-table.css",
"./node_modules/easymde/dist/easymde.min.css",
"./public/css/build/app.css",
"./node_modules/select2/dist/css/select2.css",
"./public/css/build/overrides.css",
@@ -70,8 +71,8 @@ mix
.js(
[
"./resources/assets/js/snipeit.js",
"./resources/assets/js/snipeit_modals.js",
"./node_modules/canvas-confetti/dist/confetti.browser.js",
"./resources/assets/js/snipeit_modals.js",
"./node_modules/canvas-confetti/dist/confetti.browser.js",
// The general direction we have been going is to pull these via require() directly
// But this runs in only one place, is only 24k, and doesn't break the sourcemaps
// (and it needs to run in 'immediate' mode, not in 'moar_scripts'), so let's just
@@ -141,7 +142,7 @@ mix
'./resources/assets/js/FileSaver.min.js',
'./node_modules/xlsx/dist/xlsx.core.min.js',
'./node_modules/bootstrap-table/dist/extensions/sticky-header/bootstrap-table-sticky-header.js',
'./node_modules/bootstrap-table/dist/extensions/toolbar/bootstrap-table-toolbar.js'
'./node_modules/bootstrap-table/dist/extensions/toolbar/bootstrap-table-toolbar.js',
],
'public/js/dist/bootstrap-table.js'
).version();