• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / inventree-app / 19032163935

03 Nov 2025 10:57AM UTC coverage: 1.474%. Remained the same
19032163935

Pull #708

github

web-flow
Merge a646ace60 into 490d00844
Pull Request #708: Thumbnail errors

0 of 6 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

768 of 52112 relevant lines covered (1.47%)

0.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.85
/lib/api.dart
1
import "dart:async";
2
import "dart:convert";
3
import "dart:io";
4

5
import "package:flutter/foundation.dart";
6
import "package:http/http.dart" as http;
7
import "package:intl/intl.dart";
8
import "package:inventree/main.dart";
9
import "package:one_context/one_context.dart";
10
import "package:open_filex/open_filex.dart";
11
import "package:cached_network_image/cached_network_image.dart";
12
import "package:flutter/material.dart";
13
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
14
import "package:flutter_cache_manager/flutter_cache_manager.dart";
15
import "package:path_provider/path_provider.dart";
16

17
import "package:inventree/api_form.dart";
18
import "package:inventree/app_colors.dart";
19
import "package:inventree/preferences.dart";
20
import "package:inventree/l10.dart";
21
import "package:inventree/helpers.dart";
22
import "package:inventree/inventree/model.dart";
23
import "package:inventree/inventree/notification.dart";
24
import "package:inventree/inventree/status_codes.dart";
25
import "package:inventree/inventree/sentry.dart";
26
import "package:inventree/user_profile.dart";
27
import "package:inventree/widget/dialogs.dart";
28
import "package:inventree/widget/snacks.dart";
29

30
/*
31
 * Class representing an API response from the server
32
 */
33
class APIResponse {
34
  APIResponse({
4✔
35
    this.url = "",
36
    this.method = "",
37
    this.statusCode = -1,
38
    this.error = "",
39
    this.data = const {},
40
  });
41

42
  int statusCode = -1;
43

44
  String url = "";
45

46
  String method = "";
47

48
  String error = "";
49

50
  String errorDetail = "";
51

52
  dynamic data = {};
53

54
  // Request is "valid" if a statusCode was returned
55
  bool isValid() => (statusCode >= 0) && (statusCode < 500);
18✔
56

57
  bool successful() => (statusCode >= 200) && (statusCode < 300);
15✔
58

59
  bool redirected() => (statusCode >= 300) && (statusCode < 400);
×
60

61
  bool clientError() => (statusCode >= 400) && (statusCode < 500);
×
62

63
  bool serverError() => statusCode >= 500;
×
64

65
  bool isMap() {
4✔
66
    return data != null && data is Map<String, dynamic>;
12✔
67
  }
68

69
  Map<String, dynamic> asMap() {
4✔
70
    if (isMap()) {
4✔
71
      return data as Map<String, dynamic>;
3✔
72
    } else {
73
      // Empty map
74
      return {};
1✔
75
    }
76
  }
77

78
  bool isList() {
3✔
79
    return data != null && data is List<dynamic>;
9✔
80
  }
81

82
  List<dynamic> asList() {
3✔
83
    if (isList()) {
3✔
84
      return data as List<dynamic>;
3✔
85
    } else {
86
      return [];
×
87
    }
88
  }
89

90
  /*
91
   * Helper function to interpret response, and return a list.
92
   * Handles case where the response is paginated, or a complete set of results
93
   */
94
  List<dynamic> resultsList() {
×
95
    if (isList()) {
×
96
      return asList();
×
97
    } else if (isMap()) {
×
98
      var response = asMap();
×
99
      if (response.containsKey("results")) {
×
100
        return response["results"] as List<dynamic>;
×
101
      } else {
102
        return [];
×
103
      }
104
    } else {
105
      return [];
×
106
    }
107
  }
108
}
109

110
/*
111
 * Custom FileService for caching network images
112
 * Requires a custom badCertificateCallback,
113
 * so we can accept "dodgy" (e.g. self-signed) certificates
114
 */
115
class InvenTreeFileService extends FileService {
116
  InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
×
117
    _client = client ?? HttpClient();
×
118

119
    if (_client != null) {
×
120
      _client!.badCertificateCallback = (cert, host, port) {
×
121
        return !strictHttps;
122
      };
123
    }
124
  }
125

126
  HttpClient? _client;
127

128
  @override
×
129
  Future<FileServiceResponse> get(
130
    String url, {
131
    Map<String, String>? headers,
132
  }) async {
133
    final Uri resolved = Uri.base.resolve(url);
×
134

135
    final HttpClientRequest req = await _client!.getUrl(resolved);
×
136

137
    if (headers != null) {
138
      headers.forEach((key, value) {
×
139
        req.headers.add(key, value);
×
140
      });
141
    }
142

143
    final HttpClientResponse httpResponse = await req.close();
×
144

145
    final http.StreamedResponse _response = http.StreamedResponse(
×
146
      httpResponse.timeout(Duration(seconds: 60)),
×
147
      httpResponse.statusCode,
×
148
      contentLength: httpResponse.contentLength < 0
×
149
          ? 0
150
          : httpResponse.contentLength,
×
151
      reasonPhrase: httpResponse.reasonPhrase,
×
152
      isRedirect: httpResponse.isRedirect,
×
153
    );
154

155
    return HttpGetResponse(_response);
×
156
  }
157
}
158

159
/*
160
 * InvenTree API - Access to the InvenTree REST interface.
161
 *
162
 * InvenTree implements token-based authentication, which is
163
 * initialised using a username:password combination.
164
 */
165

166
/*
167
 * API class which manages all communication with the InvenTree server
168
 */
169
class InvenTreeAPI {
170
  factory InvenTreeAPI() {
4✔
171
    return _api;
4✔
172
  }
173

174
  InvenTreeAPI._internal();
4✔
175

176
  // Ensure we only ever create a single instance of the API class
177
  static final InvenTreeAPI _api = InvenTreeAPI._internal();
12✔
178

179
  // List of callback functions to trigger when the connection status changes
180
  List<Function()> _statusCallbacks = [];
181

182
  // Register a callback function to be notified when the connection status changes
183
  void registerCallback(Function() func) => _statusCallbacks.add(func);
×
184

185
  void _connectionStatusChanged() {
3✔
186
    for (Function() func in _statusCallbacks) {
3✔
187
      // Call the function
188
      func();
×
189
    }
190
  }
191

192
  // Minimum required API version for server
193
  // 2023-03-04
194
  static const _minApiVersion = 100;
195

196
  bool _strictHttps = false;
197

198
  // Endpoint for requesting an API token
199
  static const _URL_TOKEN = "user/token/";
200
  static const _URL_ROLES = "user/roles/";
201
  static const _URL_ME = "user/me/";
202

203
  // Accessors for various url endpoints
204
  String get baseUrl {
4✔
205
    String url = profile?.server ?? "";
7✔
206

207
    if (!url.endsWith("/")) {
4✔
208
      url += "/";
2✔
209
    }
210

211
    return url;
212
  }
213

214
  String _makeUrl(String url) {
4✔
215
    // Strip leading slash
216
    if (url.startsWith("/")) {
4✔
217
      url = url.substring(1, url.length);
8✔
218
    }
219

220
    // Prevent double-slash
221
    url = url.replaceAll("//", "/");
4✔
222

223
    return baseUrl + url;
8✔
224
  }
225

226
  String get apiUrl => _makeUrl("/api/");
6✔
227

228
  String get imageUrl => _makeUrl("/image/");
×
229

230
  String makeApiUrl(String endpoint) {
4✔
231
    if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
8✔
232
      return _makeUrl(endpoint);
×
233
    } else {
234
      return _makeUrl("/api/${endpoint}");
8✔
235
    }
236
  }
237

238
  String makeUrl(String endpoint) => _makeUrl(endpoint);
×
239

240
  UserProfile? profile;
241

242
  // Available user roles are loaded when connecting to the server
243
  Map<String, dynamic> roles = {};
244

245
  // Available user permissions are loaded when connecting to the server
246
  Map<String, dynamic> permissions = {};
247

248
  // Profile authentication token
249
  String get token => profile?.token ?? "";
9✔
250

251
  bool get hasToken => token.isNotEmpty;
9✔
252

253
  String? get serverAddress {
×
254
    return profile?.server;
×
255
  }
256

257
  /*
258
   * Check server connection and display messages if not connected.
259
   * Useful as a precursor check before performing operations.
260
   */
261
  bool checkConnection() {
1✔
262
    // Is the server connected?
263
    if (!isConnected()) {
1✔
264
      showSnackIcon(
1✔
265
        L10().notConnected,
2✔
266
        success: false,
267
        icon: TablerIcons.server,
268
      );
269

270
      return false;
271
    }
272

273
    // Finally
274
    return true;
275
  }
276

277
  // Map of user information
278
  Map<String, dynamic> userInfo = {};
279

280
  String get username => (userInfo["username"] ?? "") as String;
×
281

282
  int get userId => (userInfo["pk"] ?? -1) as int;
×
283

284
  // Map of server information
285
  Map<String, dynamic> serverInfo = {};
286

287
  String get serverInstance => (serverInfo["instance"] ?? "") as String;
3✔
288
  String get serverVersion => (serverInfo["version"] ?? "") as String;
12✔
289
  int get apiVersion => (serverInfo["apiVersion"] ?? 1) as int;
12✔
290

291
  // Consolidated search request API v102 or newer
292
  bool get supportsConsolidatedSearch => apiVersion >= 102;
×
293

294
  // ReturnOrder supports API v104 or newer
295
  bool get supportsReturnOrders => apiVersion >= 104;
×
296

297
  // "Contact" model exposed to API
298
  bool get supportsContactModel => apiVersion >= 104;
×
299

300
  // Status label endpoints API v105 or newer
301
  bool get supportsStatusLabelEndpoints => apiVersion >= 105;
9✔
302

303
  // Regex search API v106 or newer
304
  bool get supportsRegexSearch => apiVersion >= 106;
×
305

306
  // Order barcodes API v107 or newer
307
  bool get supportsOrderBarcodes => apiVersion >= 107;
×
308

309
  // Project codes require v109 or newer
310
  bool get supportsProjectCodes => apiVersion >= 109;
×
311

312
  // Does the server support extra fields on stock adjustment actions?
313
  bool get supportsStockAdjustExtraFields => apiVersion >= 133;
×
314

315
  // Does the server support receiving items against a PO using barcodes?
316
  bool get supportsBarcodePOReceiveEndpoint => apiVersion >= 139;
×
317

318
  // Does the server support adding line items to a PO using barcodes?
319
  bool get supportsBarcodePOAddLineEndpoint => apiVersion >= 153;
×
320

321
  // Does the server support allocating stock to sales order using barcodes?
322
  bool get supportsBarcodeSOAllocateEndpoint => apiVersion >= 160;
×
323

324
  // Does the server support the "modern" test results API
325
  // Ref: https://github.com/inventree/InvenTree/pull/6430/
326
  bool get supportsModernTestResults => apiVersion >= 169;
×
327

328
  // Does the server support "null" top-level filtering for PartCategory and StockLocation endpoints?
329
  bool get supportsNullTopLevelFiltering => apiVersion < 174;
×
330

331
  // Does the server support "active" status on Company and SupplierPart API endpoints?
332
  bool get supportsCompanyActiveStatus => apiVersion >= 189;
×
333

334
  // Does the server support the "modern" (consolidated) label printing API?
335
  bool get supportsModernLabelPrinting => apiVersion >= 201;
×
336

337
  // Does the server support the "modern" (consolidated) attachment API?
338
  // Ref: https://github.com/inventree/InvenTree/pull/7420
339
  bool get supportsModernAttachments => apiVersion >= 207;
×
340

341
  bool get supportsUserPermissions => apiVersion >= 207;
9✔
342

343
  // Does the server support the "destination" field on the PurchaseOrder model?
344
  // Ref: https://github.com/inventree/InvenTree/pull/8403
345
  bool get supportsPurchaseOrderDestination => apiVersion >= 276;
×
346

347
  // Does the server support the "start_date" field for orders?
348
  // Ref: https://github.com/inventree/InvenTree/pull/8966
349
  bool get supportsStartDate => apiVersion >= 306;
×
350

351
  // Supports separate search against "supplier" / "customer" / "manufacturer"
352
  bool get supportsSplitCompanySearch => apiVersion >= 315;
×
353

354
  // Cached list of plugins (refreshed when we connect to the server)
355
  List<InvenTreePlugin> _plugins = [];
356

357
  // Return a list of plugins enabled on the server
358
  // Can optionally filter by a particular 'mixin' type
359
  List<InvenTreePlugin> getPlugins({String mixin = ""}) {
×
360
    List<InvenTreePlugin> plugins = [];
×
361

362
    for (var plugin in _plugins) {
×
363
      // Do we wish to filter by a particular mixin?
364
      if (mixin.isNotEmpty) {
×
365
        if (!plugin.supportsMixin(mixin)) {
×
366
          continue;
367
        }
368
      }
369

370
      plugins.add(plugin);
×
371
    }
372

373
    // Return list of matching plugins
374
    return plugins;
375
  }
376

377
  // Test if the provided plugin mixin is supported by any active plugins
378
  bool supportsMixin(String mixin) => getPlugins(mixin: mixin).isNotEmpty;
×
379

380
  // Connection status flag - set once connection has been validated
381
  bool _connected = false;
382

383
  bool _connecting = false;
384

385
  bool isConnected() {
3✔
386
    return profile != null && _connected && baseUrl.isNotEmpty && hasToken;
9✔
387
  }
388

389
  bool isConnecting() {
1✔
390
    return !isConnected() && _connecting;
2✔
391
  }
392

393
  /*
394
   * Perform the required login steps, in sequence.
395
   * Internal function, called by connectToServer()
396
   *
397
   * Performs the following steps:
398
   *
399
   * 1. Check the api/ endpoint to see if the sever exists
400
   * 2. If no token available, perform user authentication
401
   * 2. Check the api/user/me/ endpoint to see if the user is authenticated
402
   * 3. If not authenticated, purge token, and exit
403
   * 4. Request user roles
404
   * 5. Request information on available plugins
405
   */
406
  Future<bool> _connectToServer() async {
3✔
407
    if (!await _checkServer()) {
3✔
408
      return false;
409
    }
410

411
    if (!hasToken) {
3✔
412
      return false;
413
    }
414

415
    if (!await _checkAuth()) {
3✔
416
      showServerError(
×
417
        _URL_ME,
418
        L10().serverNotConnected,
×
419
        L10().serverAuthenticationError,
×
420
      );
421

422
      // Invalidate the token
423
      if (profile != null) {
×
424
        profile!.token = "";
×
425
        await UserProfileDBManager().updateProfile(profile!);
×
426
      }
427

428
      return false;
429
    }
430

431
    if (!await _fetchRoles()) {
3✔
432
      return false;
433
    }
434

435
    if (!await _fetchPlugins()) {
3✔
436
      return false;
437
    }
438

439
    // Finally, connected
440
    return true;
441
  }
442

443
  /*
444
   * Check that the remote server is available.
445
   * Ping the api/ endpoint, which does not require user authentication
446
   */
447
  Future<bool> _checkServer() async {
3✔
448
    String address = profile?.server ?? "";
6✔
449

450
    if (address.isEmpty) {
3✔
451
      showSnackIcon(
×
452
        L10().incompleteDetails,
×
453
        icon: TablerIcons.exclamation_circle,
454
        success: false,
455
      );
456
      return false;
457
    }
458

459
    if (!address.endsWith("/")) {
3✔
460
      address = address + "/";
1✔
461
    }
462

463
    // Cache the "strictHttps" setting, so we can use it later without async requirement
464
    _strictHttps =
3✔
465
        await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
6✔
466
            as bool;
467

468
    debug("Connecting to ${apiUrl}");
9✔
469

470
    APIResponse response = await get("", expectedStatusCode: 200);
3✔
471

472
    if (!response.successful()) {
3✔
473
      debug("Server returned invalid response: ${response.statusCode}");
3✔
474
      showStatusCodeError(
1✔
475
        apiUrl,
1✔
476
        response.statusCode,
1✔
477
        details: response.data.toString(),
2✔
478
      );
479
      return false;
480
    }
481

482
    Map<String, dynamic> _data = response.asMap();
3✔
483

484
    serverInfo = {..._data};
6✔
485

486
    if (serverVersion.isEmpty) {
6✔
487
      showServerError(apiUrl, L10().missingData, L10().serverMissingData);
×
488

489
      return false;
490
    }
491

492
    if (apiVersion < _minApiVersion) {
6✔
493
      String message = L10().serverApiVersion + ": ${apiVersion}";
×
494

495
      message += "\n";
×
496
      message += L10().serverApiRequired + ": ${_minApiVersion}";
×
497

498
      message += "\n\n";
×
499

500
      message += "Ensure your InvenTree server version is up to date!";
×
501

502
      showServerError(apiUrl, L10().serverOld, message);
×
503

504
      return false;
505
    }
506

507
    // At this point, we have a server which is responding
508
    return true;
509
  }
510

511
  /*
512
   * Check that the user is authenticated
513
   * Fetch the user information
514
   */
515
  Future<bool> _checkAuth() async {
3✔
516
    debug("Checking user auth @ ${_URL_ME}");
3✔
517

518
    userInfo.clear();
6✔
519

520
    final response = await get(_URL_ME);
3✔
521

522
    if (response.successful() && response.statusCode == 200) {
9✔
523
      userInfo = response.asMap();
6✔
524
      return true;
525
    } else {
526
      debug(
×
527
        "Auth request failed: Server returned status ${response.statusCode}",
×
528
      );
529
      if (response.data != null) {
×
530
        debug("Server response: ${response.data.toString()}");
×
531
      }
532

533
      return false;
534
    }
535
  }
536

537
  /*
538
   * Fetch a token from the server,
539
   * with a temporary authentication header
540
   */
541
  Future<APIResponse> fetchToken(
3✔
542
    UserProfile userProfile,
543
    String username,
544
    String password,
545
  ) async {
546
    debug("Fetching user token from ${userProfile.server}");
9✔
547

548
    profile = userProfile;
3✔
549

550
    // Form a name to request the token with
551
    String platform_name = "inventree-mobile-app";
552

553
    final deviceInfo = await getDeviceInfo();
3✔
554

555
    if (Platform.isAndroid) {
3✔
556
      platform_name += "-android";
×
557
    } else if (Platform.isIOS) {
3✔
558
      platform_name += "-ios";
×
559
    } else if (Platform.isMacOS) {
3✔
560
      platform_name += "-macos";
×
561
    } else if (Platform.isLinux) {
3✔
562
      platform_name += "-linux";
3✔
563
    } else if (Platform.isWindows) {
×
564
      platform_name += "-windows";
×
565
    }
566

567
    if (deviceInfo.containsKey("name")) {
3✔
568
      platform_name += "-" + (deviceInfo["name"] as String);
×
569
    }
570

571
    if (deviceInfo.containsKey("model")) {
3✔
572
      platform_name += "-" + (deviceInfo["model"] as String);
×
573
    }
574

575
    if (deviceInfo.containsKey("systemVersion")) {
3✔
576
      platform_name += "-" + (deviceInfo["systemVersion"] as String);
×
577
    }
578

579
    // Construct auth header from username and password
580
    String authHeader =
581
        "Basic " + base64Encode(utf8.encode("${username}:${password}"));
12✔
582

583
    // Perform request to get a token
584
    final response = await get(
3✔
585
      _URL_TOKEN,
586
      params: {"name": platform_name},
3✔
587
      headers: {HttpHeaders.authorizationHeader: authHeader},
3✔
588
    );
589

590
    // Invalid response
591
    if (!response.successful()) {
3✔
592
      switch (response.statusCode) {
1✔
593
        case 401:
1✔
594
        case 403:
×
595
          showServerError(
1✔
596
            apiUrl,
1✔
597
            L10().serverAuthenticationError,
2✔
598
            L10().invalidUsernamePassword,
2✔
599
          );
600
        default:
601
          showStatusCodeError(apiUrl, response.statusCode);
×
602
      }
603

604
      debug("Token request failed: STATUS ${response.statusCode}");
3✔
605

606
      if (response.data != null) {
1✔
607
        debug("Response data: ${response.data.toString()}");
4✔
608
      }
609
    }
610

611
    final data = response.asMap();
3✔
612

613
    if (!data.containsKey("token")) {
3✔
614
      showServerError(
1✔
615
        apiUrl,
1✔
616
        L10().tokenMissing,
2✔
617
        L10().tokenMissingFromResponse,
2✔
618
      );
619
    }
620

621
    // Save the token to the user profile
622
    userProfile.token = (data["token"] ?? "") as String;
6✔
623

624
    debug("Received token from server: ${userProfile.token}");
9✔
625

626
    await UserProfileDBManager().updateProfile(userProfile);
6✔
627

628
    return response;
629
  }
630

631
  void disconnectFromServer() {
3✔
632
    debug("API : disconnectFromServer()");
3✔
633

634
    _connected = false;
3✔
635
    _connecting = false;
3✔
636
    profile = null;
3✔
637

638
    // Clear received settings
639
    _globalSettings.clear();
6✔
640
    _userSettings.clear();
6✔
641

642
    roles.clear();
6✔
643
    _plugins.clear();
6✔
644
    serverInfo.clear();
6✔
645
    _connectionStatusChanged();
3✔
646
  }
647

648
  /* Public facing connection function.
649
   */
650
  Future<bool> connectToServer(UserProfile prf) async {
3✔
651
    // Ensure server is first disconnected
652
    disconnectFromServer();
3✔
653

654
    profile = prf;
3✔
655

656
    if (profile == null) {
3✔
657
      showSnackIcon(
×
658
        L10().profileSelect,
×
659
        success: false,
660
        icon: TablerIcons.exclamation_circle,
661
      );
662
      return false;
663
    }
664

665
    // Cancel notification timer
666
    _notification_timer?.cancel();
5✔
667

668
    _connecting = true;
3✔
669
    _connectionStatusChanged();
3✔
670

671
    // Perform the actual connection routine
672
    _connected = await _connectToServer();
6✔
673
    _connecting = false;
3✔
674

675
    if (_connected) {
3✔
676
      showSnackIcon(
3✔
677
        L10().serverConnected,
6✔
678
        icon: TablerIcons.server,
679
        success: true,
680
      );
681

682
      if (_notification_timer == null) {
3✔
683
        debug("starting notification timer");
3✔
684
        _notification_timer = Timer.periodic(Duration(seconds: 60), (timer) {
9✔
685
          _refreshNotifications();
×
686
        });
687
      }
688
    }
689

690
    _connectionStatusChanged();
3✔
691

692
    fetchStatusCodeData();
3✔
693

694
    return _connected;
3✔
695
  }
696

697
  /*
698
   * Request the user roles (permissions) from the InvenTree server
699
   */
700
  Future<bool> _fetchRoles() async {
3✔
701
    roles.clear();
6✔
702

703
    debug("API: Requesting user role data");
3✔
704

705
    final response = await get(_URL_ROLES, expectedStatusCode: 200);
3✔
706

707
    if (!response.successful()) {
3✔
708
      return false;
709
    }
710

711
    var data = response.asMap();
3✔
712

713
    if (!data.containsKey("roles")) {
3✔
714
      roles = {};
×
715
      permissions = {};
×
716

717
      showServerError(apiUrl, L10().serverError, L10().errorUserRoles);
×
718
      return false;
719
    }
720

721
    roles = (data["roles"] ?? {}) as Map<String, dynamic>;
6✔
722

723
    if (supportsUserPermissions && data.containsKey("permissions")) {
6✔
724
      permissions = (data["permissions"] ?? {}) as Map<String, dynamic>;
6✔
725
    } else {
726
      permissions = {};
×
727
    }
728

729
    return true;
730
  }
731

732
  // Request plugin information from the server
733
  Future<bool> _fetchPlugins() async {
3✔
734
    _plugins.clear();
6✔
735

736
    debug("API: getPluginInformation()");
3✔
737

738
    // Request a list of plugins from the server
739
    final List<InvenTreeModel> results = await InvenTreePlugin().list();
6✔
740

741
    for (var result in results) {
6✔
742
      if (result is InvenTreePlugin) {
3✔
743
        if (result.active) {
3✔
744
          // Only add plugins that are active
745
          _plugins.add(result);
6✔
746
        }
747
      }
748
    }
749

750
    return true;
751
  }
752

753
  /*
754
   * Check if the user has the given role.permission assigned
755
   * e.g. "sales_order", "change"
756
   */
757
  bool checkRole(String role, String permission) {
1✔
758
    if (!_connected) {
1✔
759
      return false;
760
    }
761

762
    // If we do not have enough information, assume permission is allowed
763
    if (roles.isEmpty) {
2✔
764
      debug("checkRole - no roles defined!");
×
765
      return true;
766
    }
767

768
    if (!roles.containsKey(role)) {
2✔
769
      debug("checkRole - role '$role' not found!");
2✔
770
      return true;
771
    }
772

773
    if (roles[role] == null) {
2✔
774
      debug("checkRole - role '$role' is null!");
×
775
      return false;
776
    }
777

778
    try {
779
      List<String> perms = List.from(roles[role] as List<dynamic>);
3✔
780
      return perms.contains(permission);
1✔
781
    } catch (error, stackTrace) {
782
      if (error is TypeError) {
×
783
        // Ignore TypeError
784
      } else {
785
        // Unknown error - report it!
786
        sentryReportError(
×
787
          "api.checkRole",
788
          error,
789
          stackTrace,
790
          context: {
×
791
            "role": role,
792
            "permission": permission,
793
            "error": error.toString(),
×
794
          },
795
        );
796
      }
797

798
      // Unable to determine permission - assume true?
799
      return true;
800
    }
801
  }
802

803
  /*
804
   * Check if the user has the particular model permission assigned
805
   * e.g. "company", "add"
806
   */
807
  bool checkPermission(String model, String permission) {
×
808
    if (!_connected) {
×
809
      return false;
810
    }
811

812
    if (permissions.isEmpty) {
×
813
      // Not enough information available - default to True
814
      return true;
815
    }
816

817
    if (!permissions.containsKey(model)) {
×
818
      debug("checkPermission - model '$model' not found!");
×
819
      return false;
820
    }
821

822
    if (permissions[model] == null) {
×
823
      debug("checkPermission - model '$model' is null!");
×
824
      return false;
825
    }
826

827
    try {
828
      List<String> perms = List.from(permissions[model] as List<dynamic>);
×
829
      return perms.contains(permission);
×
830
    } catch (error, stackTrace) {
831
      if (error is TypeError) {
×
832
        // Ignore TypeError
833
      } else {
834
        // Unknown error - report it!
835
        sentryReportError(
×
836
          "api.checkPermission",
837
          error,
838
          stackTrace,
839
          context: {
×
840
            "model": model,
841
            "permission": permission,
842
            "error": error.toString(),
×
843
          },
844
        );
845
      }
846

847
      // Unable to determine permission - assume true?
848
      return true;
849
    }
850
  }
851

852
  // Perform a PATCH request
853
  Future<APIResponse> patch(
2✔
854
    String url, {
855
    Map<String, dynamic> body = const {},
856
    int? expectedStatusCode,
857
  }) async {
858
    Map<String, dynamic> _body = body;
859

860
    HttpClientRequest? request = await apiRequest(url, "PATCH");
2✔
861

862
    if (request == null) {
863
      // Return an "invalid" APIResponse
864
      return APIResponse(
×
865
        url: url,
866
        method: "PATCH",
867
        error: "HttpClientRequest is null",
868
      );
869
    }
870

871
    return completeRequest(
2✔
872
      request,
873
      data: json.encode(_body),
2✔
874
      statusCode: expectedStatusCode,
875
    );
876
  }
877

878
  /*
879
   * Download a file from the given URL
880
   */
881
  Future<void> downloadFile(String url, {bool openOnDownload = true}) async {
×
882
    if (url.isEmpty) {
×
883
      // No URL provided for download
884
      return;
885
    }
886

887
    // Find the local downloads directory
888
    final Directory dir = await getTemporaryDirectory();
×
889

890
    String filename = url.split("/").last;
×
891

892
    String local_path = dir.path + "/" + filename;
×
893

894
    Uri? _uri = Uri.tryParse(makeUrl(url));
×
895

896
    if (_uri == null) {
897
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
898
      return;
899
    }
900

901
    if (_uri.host.isEmpty) {
×
902
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
903
      return;
904
    }
905

906
    HttpClientRequest? _request;
907

908
    final bool strictHttps =
909
        await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
×
910
            as bool;
911

912
    var client = createClient(url, strictHttps: strictHttps);
×
913

914
    // Attempt to open a connection to the server
915
    try {
916
      _request = await client
917
          .openUrl("GET", _uri)
×
918
          .timeout(Duration(seconds: 10));
×
919

920
      // Set headers
921
      defaultHeaders().forEach((key, value) {
×
922
        _request?.headers.set(key, value);
×
923
      });
924
    } on SocketException catch (error) {
×
925
      debug("SocketException at ${url}: ${error.toString()}");
×
926
      showServerError(url, L10().connectionRefused, error.toString());
×
927
      return;
928
    } on TimeoutException {
×
929
      debug("TimeoutException at ${url}");
×
930
      showTimeoutError(url);
×
931
      return;
932
    } on HandshakeException catch (error) {
×
933
      debug("HandshakeException at ${url}:");
×
934
      debug(error.toString());
×
935
      showServerError(url, L10().serverCertificateError, error.toString());
×
936
      return;
937
    } catch (error, stackTrace) {
938
      debug("Server error at ${url}: ${error.toString()}");
×
939
      showServerError(url, L10().serverError, error.toString());
×
940
      sentryReportError("api.downloadFile : client.openUrl", error, stackTrace);
×
941
      return;
942
    }
943

944
    try {
945
      final response = await _request.close();
×
946

947
      if (response.statusCode == 200) {
×
948
        var bytes = await consolidateHttpClientResponseBytes(response);
×
949

950
        File localFile = File(local_path);
×
951

952
        await localFile.writeAsBytes(bytes);
×
953

954
        if (openOnDownload) {
955
          OpenFilex.open(local_path);
×
956
        }
957
      } else {
958
        showStatusCodeError(url, response.statusCode);
×
959
      }
960
    } on SocketException catch (error) {
×
961
      showServerError(url, L10().connectionRefused, error.toString());
×
962
    } on TimeoutException {
×
963
      showTimeoutError(url);
×
964
    } catch (error, stackTrace) {
965
      debug("Error downloading image:");
×
966
      debug(error.toString());
×
967
      showServerError(url, L10().downloadError, error.toString());
×
968
      sentryReportError(
×
969
        "api.downloadFile : client.closeRequest",
970
        error,
971
        stackTrace,
972
      );
973
    }
974
  }
975

976
  /*
977
   * Upload a file to the given URL
978
   */
979
  Future<APIResponse> uploadFile(
×
980
    String url,
981
    File f, {
982
    String name = "attachment",
983
    String method = "POST",
984
    Map<String, dynamic>? fields,
985
  }) async {
986
    var _url = makeApiUrl(url);
×
987

988
    var request = http.MultipartRequest(method, Uri.parse(_url));
×
989

990
    request.headers.addAll(defaultHeaders());
×
991

992
    if (fields != null) {
993
      fields.forEach((String key, dynamic value) {
×
994
        if (value == null) {
995
          request.fields[key] = "";
×
996
        } else {
997
          request.fields[key] = value.toString();
×
998
        }
999
      });
1000
    }
1001

1002
    var _file = await http.MultipartFile.fromPath(name, f.path);
×
1003

1004
    request.files.add(_file);
×
1005

1006
    APIResponse response = APIResponse(url: url, method: method);
×
1007

1008
    String jsondata = "";
1009

1010
    try {
1011
      var httpResponse = await request.send().timeout(Duration(seconds: 120));
×
1012

1013
      response.statusCode = httpResponse.statusCode;
×
1014

1015
      jsondata = await httpResponse.stream.bytesToString();
×
1016

1017
      response.data = json.decode(jsondata);
×
1018

1019
      // Report a server-side error
1020
      if (response.statusCode == 500) {
×
1021
        sentryReportMessage(
×
1022
          "Server error in uploadFile()",
1023
          context: {
×
1024
            "url": url,
1025
            "method": request.method,
×
1026
            "name": name,
1027
            "statusCode": response.statusCode.toString(),
×
1028
            "requestHeaders": request.headers.toString(),
×
1029
            "responseHeaders": httpResponse.headers.toString(),
×
1030
          },
1031
        );
1032
      }
1033
    } on SocketException catch (error) {
×
1034
      showServerError(url, L10().connectionRefused, error.toString());
×
1035
      response.error = "SocketException";
×
1036
      response.errorDetail = error.toString();
×
1037
    } on FormatException {
×
1038
      showServerError(
×
1039
        url,
1040
        L10().formatException,
×
1041
        L10().formatExceptionJson + ":\n${jsondata}",
×
1042
      );
1043

1044
      sentryReportMessage(
×
1045
        "Error decoding JSON response from server",
1046
        context: {
×
1047
          "method": "uploadFile",
1048
          "url": url,
1049
          "statusCode": response.statusCode.toString(),
×
1050
          "data": jsondata,
1051
        },
1052
      );
1053
    } on TimeoutException {
×
1054
      showTimeoutError(url);
×
1055
      response.error = "TimeoutException";
×
1056
    } catch (error, stackTrace) {
1057
      showServerError(url, L10().serverError, error.toString());
×
1058
      sentryReportError("api.uploadFile", error, stackTrace);
×
1059
      response.error = "UnknownError";
×
1060
      response.errorDetail = error.toString();
×
1061
    }
1062

1063
    return response;
1064
  }
1065

1066
  /*
1067
   * Perform a HTTP OPTIONS request,
1068
   * to get the available fields at a given endpoint.
1069
   * We send this with the currently selected "locale",
1070
   * so that (hopefully) the field messages are correctly translated
1071
   */
1072
  Future<APIResponse> options(String url) async {
×
1073
    HttpClientRequest? request = await apiRequest(url, "OPTIONS");
×
1074

1075
    if (request == null) {
1076
      // Return an "invalid" APIResponse
1077
      return APIResponse(url: url, method: "OPTIONS");
×
1078
    }
1079

1080
    return completeRequest(request);
×
1081
  }
1082

1083
  /*
1084
   * Perform a HTTP POST request
1085
   * Returns a json object (or null if unsuccessful)
1086
   */
1087
  Future<APIResponse> post(
2✔
1088
    String url, {
1089
    Map<String, dynamic> body = const {},
1090
    int? expectedStatusCode = 201,
1091
  }) async {
1092
    HttpClientRequest? request = await apiRequest(url, "POST");
2✔
1093

1094
    if (request == null) {
1095
      // Return an "invalid" APIResponse
1096
      return APIResponse(url: url, method: "POST");
1✔
1097
    }
1098

1099
    return completeRequest(
1✔
1100
      request,
1101
      data: json.encode(body),
1✔
1102
      statusCode: expectedStatusCode,
1103
    );
1104
  }
1105

1106
  /*
1107
   * Perform a request to link a custom barcode to a particular item
1108
   */
1109
  Future<bool> linkBarcode(Map<String, String> body) async {
1✔
1110
    HttpClientRequest? request = await apiRequest("/barcode/link/", "POST");
1✔
1111

1112
    if (request == null) {
1113
      return false;
1114
    }
1115

1116
    final response = await completeRequest(
1✔
1117
      request,
1118
      data: json.encode(body),
1✔
1119
      statusCode: 200,
1120
    );
1121

1122
    return response.isValid() && response.statusCode == 200;
3✔
1123
  }
1124

1125
  /*
1126
   * Perform a request to unlink a custom barcode from a particular item
1127
   */
1128
  Future<bool> unlinkBarcode(Map<String, dynamic> body) async {
1✔
1129
    HttpClientRequest? request = await apiRequest("/barcode/unlink/", "POST");
1✔
1130

1131
    if (request == null) {
1132
      return false;
1133
    }
1134

1135
    final response = await completeRequest(
1✔
1136
      request,
1137
      data: json.encode(body),
1✔
1138
      statusCode: 200,
1139
    );
1140

1141
    return response.isValid() && response.statusCode == 200;
3✔
1142
  }
1143

1144
  HttpClient createClient(String url, {bool strictHttps = false}) {
3✔
1145
    var client = HttpClient();
3✔
1146

1147
    client.badCertificateCallback =
3✔
1148
        (X509Certificate cert, String host, int port) {
×
1149
          if (strictHttps) {
1150
            showServerError(
×
1151
              url,
1152
              L10().serverCertificateError,
×
1153
              L10().serverCertificateInvalid,
×
1154
            );
1155
            return false;
1156
          }
1157

1158
          // Strict HTTPs not enforced, so we'll ignore the bad cert
1159
          return true;
1160
        };
1161

1162
    // Set the connection timeout
1163
    client.connectionTimeout = Duration(seconds: 30);
6✔
1164

1165
    return client;
1166
  }
1167

1168
  /*
1169
   * Initiate a HTTP request to the server
1170
   *
1171
   * @param url is the API endpoint
1172
   * @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
1173
   * @param params is the request parameters
1174
   */
1175
  Future<HttpClientRequest?> apiRequest(
4✔
1176
    String url,
1177
    String method, {
1178
    Map<String, String> urlParams = const {},
1179
    Map<String, String> headers = const {},
1180
  }) async {
1181
    var _url = makeApiUrl(url);
4✔
1182

1183
    if (_url.isEmpty) {
4✔
1184
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
1185
      return null;
1186
    }
1187

1188
    // Add any required query parameters to the URL using ?key=value notation
1189
    if (urlParams.isNotEmpty) {
4✔
1190
      String query = "?";
1191

1192
      urlParams.forEach((k, v) => query += "${k}=${v}&");
12✔
1193

1194
      _url += query;
3✔
1195
    }
1196

1197
    // Remove extraneous character if present
1198
    if (_url.endsWith("&")) {
4✔
1199
      _url = _url.substring(0, _url.length - 1);
9✔
1200
    }
1201

1202
    Uri? _uri = Uri.tryParse(_url);
4✔
1203

1204
    if (_uri == null || _uri.host.isEmpty) {
8✔
1205
      showServerError(_url, L10().invalidHost, L10().invalidHostDetails);
5✔
1206
      return null;
1207
    }
1208

1209
    HttpClientRequest? _request;
1210

1211
    final bool strictHttps =
1212
        await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
6✔
1213
            as bool;
1214

1215
    var client = createClient(url, strictHttps: strictHttps);
3✔
1216

1217
    // Attempt to open a connection to the server
1218
    try {
1219
      _request = await client
1220
          .openUrl(method, _uri)
3✔
1221
          .timeout(Duration(seconds: 10));
6✔
1222

1223
      // Default headers
1224
      defaultHeaders().forEach((key, value) {
9✔
1225
        _request?.headers.set(key, value);
6✔
1226
      });
1227

1228
      // Custom headers
1229
      headers.forEach((key, value) {
6✔
1230
        _request?.headers.set(key, value);
6✔
1231
      });
1232

1233
      return _request;
1234
    } on SocketException catch (error) {
1✔
1235
      debug("SocketException at ${url}: ${error.toString()}");
3✔
1236
      showServerError(url, L10().connectionRefused, error.toString());
4✔
1237
      return null;
1238
    } on TimeoutException {
×
1239
      debug("TimeoutException at ${url}");
×
1240
      showTimeoutError(url);
×
1241
      return null;
1242
    } on OSError catch (error) {
×
1243
      debug("OSError at ${url}: ${error.toString()}");
×
1244
      showServerError(url, L10().connectionRefused, error.toString());
×
1245
      return null;
1246
    } on CertificateException catch (error) {
×
1247
      debug("CertificateException at ${url}:");
×
1248
      debug(error.toString());
×
1249
      showServerError(url, L10().serverCertificateError, error.toString());
×
1250
      return null;
1251
    } on HandshakeException catch (error) {
×
1252
      debug("HandshakeException at ${url}:");
×
1253
      debug(error.toString());
×
1254
      showServerError(url, L10().serverCertificateError, error.toString());
×
1255
      return null;
1256
    } catch (error, stackTrace) {
1257
      debug("Server error at ${url}: ${error.toString()}");
×
1258
      showServerError(url, L10().serverError, error.toString());
×
1259
      sentryReportError(
×
1260
        "api.apiRequest : openUrl",
1261
        error,
1262
        stackTrace,
1263
        context: {"url": url, "method": method},
×
1264
      );
1265

1266
      return null;
1267
    }
1268
  }
1269

1270
  /*
1271
   * Complete an API request, and return an APIResponse object
1272
   */
1273
  Future<APIResponse> completeRequest(
3✔
1274
    HttpClientRequest request, {
1275
    String? data,
1276
    int? statusCode,
1277
    bool ignoreResponse = false,
1278
  }) async {
1279
    if (data != null && data.isNotEmpty) {
2✔
1280
      var encoded_data = utf8.encode(data);
2✔
1281

1282
      request.headers.set(
4✔
1283
        HttpHeaders.contentLengthHeader,
1284
        encoded_data.length.toString(),
4✔
1285
      );
1286
      request.add(encoded_data);
2✔
1287
    }
1288

1289
    APIResponse response = APIResponse(
3✔
1290
      method: request.method,
3✔
1291
      url: request.uri.toString(),
6✔
1292
    );
1293

1294
    String url = request.uri.toString();
6✔
1295

1296
    try {
1297
      HttpClientResponse? _response = await request.close().timeout(
6✔
1298
        Duration(seconds: 10),
3✔
1299
      );
1300

1301
      response.statusCode = _response.statusCode;
6✔
1302

1303
      // If the server returns a server error code, alert the user
1304
      if (_response.statusCode >= 500) {
6✔
1305
        showStatusCodeError(url, _response.statusCode);
×
1306

1307
        // Some server errors are not ones for us to worry about!
1308
        switch (_response.statusCode) {
×
1309
          case 502: // Bad gateway
×
1310
          case 503: // Service unavailable
×
1311
          case 504: // Gateway timeout
×
1312
            break;
1313
          default: // Any other error code
1314
            sentryReportMessage(
×
1315
              "Server error",
1316
              context: {
×
1317
                "url": request.uri.toString(),
×
1318
                "method": request.method,
×
1319
                "statusCode": _response.statusCode.toString(),
×
1320
                "requestHeaders": request.headers.toString(),
×
1321
                "responseHeaders": _response.headers.toString(),
×
1322
                "responseData": response.data.toString(),
×
1323
              },
1324
            );
1325
        }
1326
      } else {
1327
        response.data = ignoreResponse
3✔
1328
            ? {}
×
1329
            : await responseToJson(url, _response) ?? {};
3✔
1330

1331
        // First check that the returned status code is what we expected
1332
        if (statusCode != null && statusCode != _response.statusCode) {
4✔
1333
          showStatusCodeError(
×
1334
            url,
1335
            _response.statusCode,
×
1336
            details: response.data.toString(),
×
1337
          );
1338
        }
1339
      }
1340
    } on HttpException catch (error) {
×
1341
      showServerError(url, L10().serverError, error.toString());
×
1342
      response.error = "HTTPException";
×
1343
      response.errorDetail = error.toString();
×
1344
    } on SocketException catch (error) {
×
1345
      showServerError(url, L10().connectionRefused, error.toString());
×
1346
      response.error = "SocketException";
×
1347
      response.errorDetail = error.toString();
×
1348
    } on CertificateException catch (error) {
×
1349
      debug("CertificateException at ${request.uri.toString()}:");
×
1350
      debug(error.toString());
×
1351
      showServerError(url, L10().serverCertificateError, error.toString());
×
1352
    } on TimeoutException {
×
1353
      showTimeoutError(url);
×
1354
      response.error = "TimeoutException";
×
1355
    } catch (error, stackTrace) {
1356
      showServerError(url, L10().serverError, error.toString());
×
1357
      sentryReportError("api.completeRequest", error, stackTrace);
×
1358
      response.error = "UnknownError";
×
1359
      response.errorDetail = error.toString();
×
1360
    }
1361

1362
    return response;
1363
  }
1364

1365
  /*
1366
   * Convert a HttpClientResponse response object to JSON
1367
   */
1368
  dynamic responseToJson(String url, HttpClientResponse response) async {
3✔
1369
    String body = await response.transform(utf8.decoder).join();
9✔
1370

1371
    try {
1372
      var data = json.decode(body);
3✔
1373

1374
      return data ?? {};
×
1375
    } on FormatException {
×
1376
      switch (response.statusCode) {
×
1377
        case 400:
×
1378
        case 401:
×
1379
        case 403:
×
1380
        case 404:
×
1381
          // Ignore for unauthorized pages
1382
          break;
1383
        case 502:
×
1384
        case 503:
×
1385
        case 504:
×
1386
          // Ignore for server errors
1387
          break;
1388
        default:
1389
          sentryReportMessage(
×
1390
            "Error decoding JSON response from server",
1391
            context: {
×
1392
              "headers": response.headers.toString(),
×
1393
              "statusCode": response.statusCode.toString(),
×
1394
              "data": body.toString(),
×
1395
              "endpoint": url,
1396
            },
1397
          );
1398
      }
1399

1400
      showServerError(
×
1401
        url,
1402
        L10().formatException,
×
1403
        L10().formatExceptionJson + ":\n${body}",
×
1404
      );
1405

1406
      // Return an empty map
1407
      return {};
×
1408
    }
1409
  }
1410

1411
  /*
1412
   * Perform a HTTP GET request
1413
   * Returns a json object (or null if did not complete)
1414
   */
1415
  Future<APIResponse> get(
3✔
1416
    String url, {
1417
    Map<String, String> params = const {},
1418
    Map<String, String> headers = const {},
1419
    int? expectedStatusCode = 200,
1420
  }) async {
1421
    HttpClientRequest? request = await apiRequest(
3✔
1422
      url,
1423
      "GET",
1424
      urlParams: params,
1425
      headers: headers,
1426
    );
1427

1428
    if (request == null) {
1429
      // Return an "invalid" APIResponse
1430
      return APIResponse(
1✔
1431
        url: url,
1432
        method: "GET",
1433
        error: "HttpClientRequest is null",
1434
      );
1435
    }
1436

1437
    return completeRequest(request);
3✔
1438
  }
1439

1440
  /*
1441
   * Perform a HTTP DELETE request
1442
   */
1443
  Future<APIResponse> delete(String url) async {
×
1444
    HttpClientRequest? request = await apiRequest(url, "DELETE");
×
1445

1446
    if (request == null) {
1447
      // Return an "invalid" APIResponse object
1448
      return APIResponse(
×
1449
        url: url,
1450
        method: "DELETE",
1451
        error: "HttpClientRequest is null",
1452
      );
1453
    }
1454

1455
    return completeRequest(request, ignoreResponse: true);
×
1456
  }
1457

1458
  // Find the current locale code for the running app
1459
  String get currentLocale {
3✔
1460
    if (hasContext()) {
3✔
1461
      // Try to get app context
1462
      BuildContext? context = OneContext().context;
×
1463

1464
      if (context != null) {
1465
        Locale? locale = InvenTreeApp.of(context)?.locale;
×
1466

1467
        if (locale != null) {
1468
          return locale.languageCode; //.toString();
×
1469
        }
1470
      }
1471
    }
1472

1473
    // Fallback value
1474
    return Intl.getCurrentLocale();
3✔
1475
  }
1476

1477
  // Return a list of request headers
1478
  Map<String, String> defaultHeaders() {
3✔
1479
    Map<String, String> headers = {};
3✔
1480

1481
    if (hasToken) {
3✔
1482
      headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
6✔
1483
    }
1484

1485
    headers[HttpHeaders.acceptHeader] = "application/json";
3✔
1486
    headers[HttpHeaders.contentTypeHeader] = "application/json";
3✔
1487
    headers[HttpHeaders.acceptLanguageHeader] = currentLocale;
6✔
1488

1489
    return headers;
1490
  }
1491

1492
  // Construct a token authorization header
1493
  String _authorizationHeader() {
3✔
1494
    if (token.isNotEmpty) {
6✔
1495
      return "Token ${token}";
6✔
1496
    } else {
1497
      return "";
1498
    }
1499
  }
1500

1501
  static String get staticImage => "/static/img/blank_image.png";
×
1502

1503
  static String get staticThumb => "/static/img/blank_image.thumbnail.png";
×
1504

1505
  CachedNetworkImage? getThumbnail(
×
1506
    String imageUrl, {
1507
    double size = 40,
1508
    bool hideIfNull = false,
1509
  }) {
1510
    if (hideIfNull) {
1511
      if (imageUrl.isEmpty) {
×
1512
        return null;
1513
      }
1514
    }
1515

1516
    try {
1517
      return getImage(imageUrl, width: size, height: size);
×
1518
    } catch (error, stackTrace) {
1519
      sentryReportError("_getThumbnail", error, stackTrace);
×
1520
      return null;
1521
    }
1522
  }
1523

1524
  /*
1525
   * Load image from the InvenTree server,
1526
   * or from local cache (if it has been cached!)
1527
   */
1528
  CachedNetworkImage getImage(
×
1529
    String imageUrl, {
1530
    double? height,
1531
    double? width,
1532
  }) {
1533
    if (imageUrl.isEmpty) {
×
1534
      imageUrl = staticImage;
×
1535
    }
1536

1537
    String url = makeUrl(imageUrl);
×
1538

1539
    const key = "inventree_network_image";
1540

1541
    CacheManager manager = CacheManager(
×
1542
      Config(key, fileService: InvenTreeFileService(strictHttps: _strictHttps)),
×
1543
    );
1544

1545
    return CachedNetworkImage(
×
1546
      imageUrl: url,
1547
      placeholder: (context, url) => CircularProgressIndicator(),
×
NEW
1548
      errorWidget: (context, url, error) {
×
NEW
1549
        print("CachedNetworkimage error: ${error.toString()}");
×
NEW
1550
        return GestureDetector(
×
NEW
1551
          child: Icon(TablerIcons.circle_x, color: COLOR_DANGER),
×
NEW
1552
          onTap: () => {
×
NEW
1553
            showSnackIcon(error.toString().split(",")[0], success: false),
×
1554
          },
1555
        );
1556
      },
UNCOV
1557
      httpHeaders: defaultHeaders(),
×
1558
      height: height,
1559
      width: width,
1560
      cacheManager: manager,
1561
    );
1562
  }
1563

1564
  // Keep a record of which settings we have received from the server
1565
  Map<String, InvenTreeGlobalSetting> _globalSettings = {};
1566
  Map<String, InvenTreeUserSetting> _userSettings = {};
1567

1568
  Future<String> getGlobalSetting(String key) async {
×
1569
    InvenTreeGlobalSetting? setting = _globalSettings[key];
×
1570

1571
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1572
      return setting.value;
×
1573
    }
1574

1575
    final response = await InvenTreeGlobalSetting().getModel(key);
×
1576

1577
    if (response is InvenTreeGlobalSetting) {
×
1578
      response.lastReload = DateTime.now();
×
1579
      _globalSettings[key] = response;
×
1580
      return response.value;
×
1581
    } else {
1582
      return "";
1583
    }
1584
  }
1585

1586
  // Return a boolean global setting value
1587
  Future<bool> getGlobalBooleanSetting(
×
1588
    String key, {
1589
    bool backup = false,
1590
  }) async {
1591
    String value = await getGlobalSetting(key);
×
1592

1593
    if (value.isEmpty) {
×
1594
      return backup;
1595
    }
1596

1597
    return value.toLowerCase().trim() == "true";
×
1598
  }
1599

1600
  Future<String> getUserSetting(String key) async {
×
1601
    InvenTreeUserSetting? setting = _userSettings[key];
×
1602

1603
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1604
      return setting.value;
×
1605
    }
1606

1607
    final response = await InvenTreeUserSetting().getModel(key);
×
1608

1609
    if (response is InvenTreeUserSetting) {
×
1610
      response.lastReload = DateTime.now();
×
1611
      _userSettings[key] = response;
×
1612
      return response.value;
×
1613
    } else {
1614
      return "";
1615
    }
1616
  }
1617

1618
  // Return a boolean user setting value
1619
  Future<bool> getUserBooleanSetting(String key) async {
×
1620
    String value = await getUserSetting(key);
×
1621
    return value.toLowerCase().trim() == "true";
×
1622
  }
1623

1624
  /*
1625
   * Send a request to the server to locate / identify either a StockItem or StockLocation
1626
   */
1627
  Future<void> locateItemOrLocation(
×
1628
    BuildContext context, {
1629
    int? item,
1630
    int? location,
1631
  }) async {
1632
    var plugins = getPlugins(mixin: "locate");
×
1633

1634
    if (plugins.isEmpty) {
×
1635
      // TODO: Error message
1636
      return;
1637
    }
1638

1639
    String plugin_name = "";
1640

1641
    if (plugins.length == 1) {
×
1642
      plugin_name = plugins.first.key;
×
1643
    } else {
1644
      // User selects which plugin to use
1645
      List<Map<String, dynamic>> plugin_options = [];
×
1646

1647
      for (var plugin in plugins) {
×
1648
        plugin_options.add({
×
1649
          "display_name": plugin.humanName,
×
1650
          "value": plugin.key,
×
1651
        });
1652
      }
1653

1654
      Map<String, dynamic> fields = {
×
1655
        "plugin": {
×
1656
          "label": L10().plugin,
×
1657
          "type": "choice",
1658
          "value": plugins.first.key,
×
1659
          "choices": plugin_options,
1660
          "required": true,
1661
        },
1662
      };
1663

1664
      await launchApiForm(
×
1665
        context,
1666
        L10().locateLocation,
×
1667
        "",
1668
        fields,
1669
        icon: TablerIcons.location_search,
1670
        onSuccess: (Map<String, dynamic> data) async {
×
1671
          plugin_name = (data["plugin"] ?? "") as String;
×
1672
        },
1673
      );
1674
    }
1675

1676
    Map<String, dynamic> body = {"plugin": plugin_name};
×
1677

1678
    if (item != null) {
1679
      body["item"] = item.toString();
×
1680
    }
1681

1682
    if (location != null) {
1683
      body["location"] = location.toString();
×
1684
    }
1685

1686
    post("/api/locate/", body: body, expectedStatusCode: 200).then((
×
1687
      APIResponse response,
1688
    ) {
1689
      if (response.successful()) {
×
1690
        showSnackIcon(L10().requestSuccessful, success: true);
×
1691
      }
1692
    });
1693
  }
1694

1695
  // Keep an internal map of status codes
1696
  Map<String, InvenTreeStatusCode> _status_codes = {};
1697

1698
  // Return a status class based on provided URL
1699
  InvenTreeStatusCode _get_status_class(String url) {
3✔
1700
    if (!_status_codes.containsKey(url)) {
6✔
1701
      _status_codes[url] = InvenTreeStatusCode(url);
9✔
1702
    }
1703

1704
    return _status_codes[url]!;
6✔
1705
  }
1706

1707
  // Accessors methods for various status code classes
1708
  InvenTreeStatusCode get StockHistoryStatus =>
3✔
1709
      _get_status_class("stock/track/status/");
3✔
1710
  InvenTreeStatusCode get StockStatus => _get_status_class("stock/status/");
6✔
1711
  InvenTreeStatusCode get PurchaseOrderStatus =>
3✔
1712
      _get_status_class("order/po/status/");
3✔
1713
  InvenTreeStatusCode get SalesOrderStatus =>
3✔
1714
      _get_status_class("order/so/status/");
3✔
1715

1716
  void clearStatusCodeData() {
×
1717
    StockHistoryStatus.data.clear();
×
1718
    StockStatus.data.clear();
×
1719
    PurchaseOrderStatus.data.clear();
×
1720
    SalesOrderStatus.data.clear();
×
1721
  }
1722

1723
  Future<void> fetchStatusCodeData({bool forceReload = true}) async {
3✔
1724
    StockHistoryStatus.load(forceReload: forceReload);
6✔
1725
    StockStatus.load(forceReload: forceReload);
6✔
1726
    PurchaseOrderStatus.load(forceReload: forceReload);
6✔
1727
    SalesOrderStatus.load(forceReload: forceReload);
6✔
1728
  }
1729

1730
  int notification_counter = 0;
1731

1732
  Timer? _notification_timer;
1733

1734
  /*
1735
   * Update notification counter (called periodically)
1736
   */
1737
  Future<void> _refreshNotifications() async {
×
1738
    if (!isConnected()) {
×
1739
      return;
1740
    }
1741

1742
    InvenTreeNotification().count(filters: {"read": "false"}).then((int n) {
×
1743
      notification_counter = n;
×
1744
    });
1745
  }
1746
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc