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

inventree / inventree-app / 12270399751

11 Dec 2024 05:40AM UTC coverage: 8.545% (-0.05%) from 8.597%
12270399751

Pull #572

github

web-flow
Merge cc0085e50 into 0ef72dc3d
Pull Request #572: Order updates

0 of 55 new or added lines in 6 files covered. (0.0%)

2 existing lines in 2 files now uncovered.

725 of 8484 relevant lines covered (8.55%)

0.3 hits per line

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

41.82
/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
/*
32
 * Class representing an API response from the server
33
 */
34
class APIResponse {
35

36
  APIResponse({this.url = "", this.method = "", this.statusCode = -1, this.error = "", this.data = const {}});
4✔
37

38
  int statusCode = -1;
39

40
  String url = "";
41

42
  String method = "";
43

44
  String error = "";
45

46
  String errorDetail = "";
47

48
  dynamic data = {};
49

50
  // Request is "valid" if a statusCode was returned
51
  bool isValid() => (statusCode >= 0) && (statusCode < 500);
18✔
52

53
  bool successful() => (statusCode >= 200) && (statusCode < 300);
15✔
54

55
  bool redirected() => (statusCode >= 300) && (statusCode < 400);
×
56

57
  bool clientError() => (statusCode >= 400) && (statusCode < 500);
×
58

59
  bool serverError() => statusCode >= 500;
×
60

61
  bool isMap() {
4✔
62
    return data != null && data is Map<String, dynamic>;
12✔
63
  }
64

65
  Map<String, dynamic> asMap() {
4✔
66
    if (isMap()) {
4✔
67
      return data as Map<String, dynamic>;
3✔
68
    } else {
69
      // Empty map
70
      return {};
1✔
71
    }
72
  }
73

74
  bool isList() {
3✔
75
    return data != null && data is List<dynamic>;
9✔
76
  }
77

78
  List<dynamic> asList() {
3✔
79
    if (isList()) {
3✔
80
      return data as List<dynamic>;
3✔
81
    } else {
82
      return [];
×
83
    }
84
  }
85

86
  /*
87
   * Helper function to interpret response, and return a list.
88
   * Handles case where the response is paginated, or a complete set of results
89
   */
90
  List<dynamic> resultsList() {
×
91

92
    if (isList()) {
×
93
      return asList();
×
94
    } else if (isMap()) {
×
95
      var response = asMap();
×
96
      if (response.containsKey("results")) {
×
97
        return response["results"] as List<dynamic>;
×
98
      } else {
99
        return [];
×
100
      }
101
    } else {
102
      return [];
×
103
    }
104
  }
105
}
106

107

108
/*
109
 * Custom FileService for caching network images
110
 * Requires a custom badCertificateCallback,
111
 * so we can accept "dodgy" (e.g. self-signed) certificates
112
 */
113
class InvenTreeFileService extends FileService {
114

115
  InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
×
116
    _client = client ?? HttpClient();
×
117

118
    if (_client != null) {
×
119
      _client!.badCertificateCallback = (cert, host, port) {
×
120
        print("BAD CERTIFICATE CALLBACK FOR IMAGE REQUEST");
×
121
        return !strictHttps;
122
      };
123
    }
124
  }
125

126
  HttpClient? _client;
127

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

133
    final HttpClientRequest req = await _client!.getUrl(resolved);
×
134

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

141
    final HttpClientResponse httpResponse = await req.close();
×
142

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

150
    return HttpGetResponse(_response);
×
151
  }
152
}
153

154
/*
155
 * InvenTree API - Access to the InvenTree REST interface.
156
 *
157
 * InvenTree implements token-based authentication, which is
158
 * initialised using a username:password combination.
159
 */
160

161

162
/*
163
 * API class which manages all communication with the InvenTree server
164
 */
165
class InvenTreeAPI {
166

167
  factory InvenTreeAPI() {
4✔
168
    return _api;
4✔
169
  }
170

171
  InvenTreeAPI._internal();
4✔
172

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

176
  // List of callback functions to trigger when the connection status changes
177
  List<Function()> _statusCallbacks = [];
178

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

182
  void _connectionStatusChanged() {
3✔
183
    for (Function() func in _statusCallbacks) {
3✔
184
      // Call the function
185
      func();
×
186
    }
187
  }
188

189
  // Minimum required API version for server
190
  // 2023-03-04
191
  static const _minApiVersion = 100;
192

193
  bool _strictHttps = false;
194

195
  // Endpoint for requesting an API token
196
  static const _URL_TOKEN = "user/token/";
197
  static const _URL_ROLES = "user/roles/";
198
  static const _URL_ME = "user/me/";
199

200
  // Accessors for various url endpoints
201
  String get baseUrl {
4✔
202
    String url = profile?.server ?? "";
7✔
203

204
    if (!url.endsWith("/")) {
4✔
205
      url += "/";
2✔
206
    }
207

208
    return url;
209
  }
210

211
  String _makeUrl(String url) {
4✔
212

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

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

221
    return baseUrl + url;
8✔
222
  }
223

224
  String get apiUrl => _makeUrl("/api/");
6✔
225

226
  String get imageUrl => _makeUrl("/image/");
×
227

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

236
  String makeUrl(String endpoint) => _makeUrl(endpoint);
×
237

238
  UserProfile? profile;
239

240
  // Available user roles (permissions) are loaded when connecting to the server
241
  Map<String, dynamic> roles = {};
242

243
  // Profile authentication token
244
  String get token => profile?.token ?? "";
9✔
245

246
  bool get hasToken => token.isNotEmpty;
9✔
247

248
  String? get serverAddress {
×
249
    return profile?.server;
×
250
  }
251

252
  /*
253
   * Check server connection and display messages if not connected.
254
   * Useful as a precursor check before performing operations.
255
   */
256
  bool checkConnection() {
1✔
257

258
    // Is the server connected?
259
    if (!isConnected()) {
1✔
260

261
      showSnackIcon(
1✔
262
        L10().notConnected,
2✔
263
        success: false,
264
        icon: TablerIcons.server
265
      );
266

267
      return false;
268
    }
269

270
    // Finally
271
    return true;
272
  }
273

274
  // Map of user information
275
  Map<String, dynamic> userInfo = {};
276

277
  String get username => (userInfo["username"] ?? "") as String;
×
278

279
  // Map of server information
280
  Map<String, dynamic> serverInfo = {};
281

282
  String get serverInstance => (serverInfo["instance"] ?? "") as String;
3✔
283
  String get serverVersion => (serverInfo["version"] ?? "") as String;
12✔
284
  int get apiVersion => (serverInfo["apiVersion"] ?? 1) as int;
12✔
285

286
  // Consolidated search request API v102 or newer
287
  bool get supportsConsolidatedSearch => isConnected() && apiVersion >= 102;
×
288

289
  // ReturnOrder supports API v104 or newer
290
  bool get supportsReturnOrders => isConnected() && apiVersion >= 104;
×
291

292
  // "Contact" model exposed to API
293
  bool get supportsContactModel => isConnected() && apiVersion >= 104;
×
294

295
  // Status label endpoints API v105 or newer
296
  bool get supportsStatusLabelEndpoints => isConnected() && apiVersion >= 105;
12✔
297

298
  // Regex search API v106 or newer
299
  bool get supportsRegexSearch => isConnected() && apiVersion >= 106;
×
300

301
  // Order barcodes API v107 or newer
302
  bool get supportsOrderBarcodes => isConnected() && apiVersion >= 107;
×
303

304
  // Project codes require v109 or newer
305
  bool get supportsProjectCodes => isConnected() && apiVersion >= 109;
×
306

307
  // Does the server support extra fields on stock adjustment actions?
308
  bool get supportsStockAdjustExtraFields => isConnected() && apiVersion >= 133;
×
309

310
  // Does the server support receiving items against a PO using barcodes?
311
  bool get supportsBarcodePOReceiveEndpoint => isConnected() && apiVersion >= 139;
×
312

313
  // Does the server support adding line items to a PO using barcodes?
314
  bool get supportsBarcodePOAddLineEndpoint => isConnected() && apiVersion >= 153;
×
315

316
  // Does the server support allocating stock to sales order using barcodes?
317
  bool get supportsBarcodeSOAllocateEndpoint => isConnected() && apiVersion >= 160;
×
318

319
  // Does the server support the "modern" test results API
320
  // Ref: https://github.com/inventree/InvenTree/pull/6430/
321
  bool get supportsModernTestResults => isConnected() && apiVersion >= 169;
×
322

323
  // Does the server support "null" top-level filtering for PartCategory and StockLocation endpoints?
324
  bool get supportsNullTopLevelFiltering => isConnected() && apiVersion < 174;
×
325

326
  // Does the server support "active" status on Company and SupplierPart API endpoints?
327
  bool get supportsCompanyActiveStatus => isConnected() && apiVersion >= 189;
×
328

329
  // Does the server support the "modern" (consolidated) label printing API?
330
  bool get supportsModernLabelPrinting => isConnected() && apiVersion >= 201;
×
331

332
  // Does the server support the "modern" (consolidated) attachment API?
333
  // Ref: https://github.com/inventree/InvenTree/pull/7420
334
  bool get supportsModernAttachments => isConnected() && apiVersion >= 207;
×
335

336
  // Does the server support the "destination" field on the PurchaseOrder model?
337
  // Ref: https://github.com/inventree/InvenTree/pull/8403
NEW
338
  bool get supportsPurchaseOrderDestination => isConnected() && apiVersion >= 276;
×
339

340
  // Cached list of plugins (refreshed when we connect to the server)
341
  List<InvenTreePlugin> _plugins = [];
342

343
  // Return a list of plugins enabled on the server
344
  // Can optionally filter by a particular 'mixin' type
345
  List<InvenTreePlugin> getPlugins({String mixin = ""}) {
×
346
    List<InvenTreePlugin> plugins = [];
×
347

348
    for (var plugin in _plugins) {
×
349
      // Do we wish to filter by a particular mixin?
350
      if (mixin.isNotEmpty) {
×
351
        if (!plugin.supportsMixin(mixin)) {
×
352
          continue;
353
        }
354
      }
355

356
      plugins.add(plugin);
×
357
    }
358

359
    // Return list of matching plugins
360
    return plugins;
361
  }
362

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

366
  // Connection status flag - set once connection has been validated
367
  bool _connected = false;
368

369
  bool _connecting = false;
370

371
  bool isConnected() {
3✔
372
    return profile != null && _connected && baseUrl.isNotEmpty && hasToken;
15✔
373
  }
374

375
  bool isConnecting() {
1✔
376
    return !isConnected() && _connecting;
2✔
377
  }
378

379

380
  /*
381
   * Perform the required login steps, in sequence.
382
   * Internal function, called by connectToServer()
383
   *
384
   * Performs the following steps:
385
   *
386
   * 1. Check the api/ endpoint to see if the sever exists
387
   * 2. If no token available, perform user authentication
388
   * 2. Check the api/user/me/ endpoint to see if the user is authenticated
389
   * 3. If not authenticated, purge token, and exit
390
   * 4. Request user roles
391
   * 5. Request information on available plugins
392
   */
393
  Future<bool> _connectToServer() async {
3✔
394

395
    if (!await _checkServer()) {
3✔
396
      return false;
397
    }
398

399
    if (!hasToken) {
3✔
400
      return false;
401
    }
402

403
    if (!await _checkAuth()) {
3✔
404
      showServerError(_URL_ME, L10().serverNotConnected, L10().serverAuthenticationError);
×
405

406
      // Invalidate the token
407
      if (profile != null) {
×
408
        profile!.token = "";
×
409
        await UserProfileDBManager().updateProfile(profile!);
×
410
      }
411

412
      return false;
413
    }
414

415
    if (!await _fetchRoles()) {
3✔
416
      return false;
417
    }
418

419
    if (!await _fetchPlugins()) {
3✔
420
      return false;
421
    }
422

423
    // Finally, connected
424
    return true;
425
  }
426

427

428
  /*
429
   * Check that the remote server is available.
430
   * Ping the api/ endpoint, which does not require user authentication
431
   */
432
  Future<bool> _checkServer() async {
3✔
433

434
    String address = profile?.server ?? "";
6✔
435

436
    if (address.isEmpty) {
3✔
437
      showSnackIcon(
×
438
          L10().incompleteDetails,
×
439
          icon: TablerIcons.exclamation_circle,
440
          success: false
441
      );
442
      return false;
443
    }
444

445
    if (!address.endsWith("/")) {
3✔
446
      address = address + "/";
1✔
447
    }
448

449
    // Cache the "strictHttps" setting, so we can use it later without async requirement
450
    _strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool;
9✔
451

452
    debug("Connecting to ${apiUrl}");
9✔
453

454
    APIResponse response = await get("", expectedStatusCode: 200);
3✔
455

456
    if (!response.successful()) {
3✔
457
      debug("Server returned invalid response: ${response.statusCode}");
3✔
458
      showStatusCodeError(apiUrl, response.statusCode, details: response.data.toString());
5✔
459
      return false;
460
    }
461

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

464
    serverInfo = {..._data};
6✔
465

466
    if (serverVersion.isEmpty) {
6✔
467
      showServerError(
×
468
        apiUrl,
×
469
        L10().missingData,
×
470
        L10().serverMissingData,
×
471
      );
472

473
      return false;
474
    }
475

476
    if (apiVersion < _minApiVersion) {
6✔
477

478
      String message = L10().serverApiVersion + ": ${apiVersion}";
×
479

480
      message += "\n";
×
481
      message += L10().serverApiRequired + ": ${_minApiVersion}";
×
482

483
      message += "\n\n";
×
484

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

487
      showServerError(
×
488
        apiUrl,
×
489
        L10().serverOld,
×
490
        message,
491
      );
492

493
      return false;
494
    }
495

496
    // At this point, we have a server which is responding
497
    return true;
498
  }
499

500

501
  /*
502
   * Check that the user is authenticated
503
   * Fetch the user information
504
   */
505
  Future<bool> _checkAuth() async {
3✔
506
    debug("Checking user auth @ ${_URL_ME}");
3✔
507

508
    userInfo.clear();
6✔
509

510
    final response = await get(_URL_ME);
3✔
511

512
    if (response.successful() && response.statusCode == 200) {
9✔
513
      userInfo = response.asMap();
6✔
514
      return true;
515
    } else {
516
      debug("Auth request failed: Server returned status ${response.statusCode}");
×
517
      if (response.data != null) {
×
518
        debug("Server response: ${response.data.toString()}");
×
519
      }
520

521
      return false;
522
    }
523
  }
524

525
  /*
526
   * Fetch a token from the server,
527
   * with a temporary authentication header
528
   */
529
  Future<APIResponse> fetchToken(UserProfile userProfile, String username, String password) async {
2✔
530

531
    debug("Fetching user token from ${userProfile.server}");
6✔
532

533
    profile = userProfile;
2✔
534

535
    // Form a name to request the token with
536
    String platform_name = "inventree-mobile-app";
537

538
    final deviceInfo = await getDeviceInfo();
2✔
539

540
    if (Platform.isAndroid) {
2✔
541
      platform_name += "-android";
×
542
    } else if (Platform.isIOS) {
2✔
543
      platform_name += "-ios";
×
544
    } else if (Platform.isMacOS) {
2✔
545
      platform_name += "-macos";
×
546
    } else if (Platform.isLinux) {
2✔
547
      platform_name += "-linux";
2✔
548
    } else if (Platform.isWindows) {
×
549
      platform_name += "-windows";
×
550
    }
551

552
    if (deviceInfo.containsKey("name")) {
2✔
553
      platform_name += "-" + (deviceInfo["name"] as String);
×
554
    }
555

556
    if (deviceInfo.containsKey("model")) {
2✔
557
      platform_name += "-" + (deviceInfo["model"] as String);
×
558
    }
559

560
    if (deviceInfo.containsKey("systemVersion")) {
2✔
561
      platform_name += "-" + (deviceInfo["systemVersion"] as String);
×
562
    }
563

564
    // Construct auth header from username and password
565
    String authHeader = "Basic " + base64Encode(utf8.encode("${username}:${password}"));
8✔
566

567
    // Perform request to get a token
568
    final response = await get(
2✔
569
        _URL_TOKEN,
570
        params: { "name": platform_name},
2✔
571
        headers: { HttpHeaders.authorizationHeader: authHeader}
2✔
572
    );
573

574
    // Invalid response
575
    if (!response.successful()) {
2✔
576
      switch (response.statusCode) {
1✔
577
        case 401:
1✔
578
        case 403:
×
579
          showServerError(
1✔
580
            apiUrl,
1✔
581
            L10().serverAuthenticationError,
2✔
582
            L10().invalidUsernamePassword,
2✔
583
          );
584
          break;
585
        default:
586
          showStatusCodeError(apiUrl, response.statusCode);
×
587
          break;
588
      }
589

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

592
      if (response.data != null) {
1✔
593
        debug("Response data: ${response.data.toString()}");
4✔
594
      }
595
    }
596

597
    final data = response.asMap();
2✔
598

599
    if (!data.containsKey("token")) {
2✔
600
      showServerError(
1✔
601
        apiUrl,
1✔
602
        L10().tokenMissing,
2✔
603
        L10().tokenMissingFromResponse,
2✔
604
      );
605
    }
606

607
    // Save the token to the user profile
608
    userProfile.token = (data["token"] ?? "") as String;
4✔
609

610
    debug("Received token from server: ${userProfile.token}");
6✔
611

612
    await UserProfileDBManager().updateProfile(userProfile);
4✔
613

614
    return response;
615
  }
616

617
  void disconnectFromServer() {
3✔
618
    debug("API : disconnectFromServer()");
3✔
619

620
    _connected = false;
3✔
621
    _connecting = false;
3✔
622
    profile = null;
3✔
623

624
    // Clear received settings
625
    _globalSettings.clear();
6✔
626
    _userSettings.clear();
6✔
627

628
    roles.clear();
6✔
629
    _plugins.clear();
6✔
630
    serverInfo.clear();
6✔
631
    _connectionStatusChanged();
3✔
632
  }
633

634

635
  /* Public facing connection function.
636
   */
637
  Future<bool> connectToServer(UserProfile prf) async {
3✔
638

639
    // Ensure server is first disconnected
640
    disconnectFromServer();
3✔
641

642
    profile = prf;
3✔
643

644
    if (profile == null) {
3✔
645
      showSnackIcon(
×
646
          L10().profileSelect,
×
647
          success: false,
648
          icon: TablerIcons.exclamation_circle
649
      );
650
      return false;
651
    }
652

653
    // Cancel notification timer
654
    _notification_timer?.cancel();
5✔
655

656
    _connecting = true;
3✔
657
    _connectionStatusChanged();
3✔
658

659
    // Perform the actual connection routine
660
    _connected = await _connectToServer();
6✔
661
    _connecting = false;
3✔
662

663
    if (_connected) {
3✔
664
      showSnackIcon(
3✔
665
        L10().serverConnected,
6✔
666
        icon: TablerIcons.server,
667
        success: true,
668
      );
669

670
      if (_notification_timer == null) {
3✔
671
        debug("starting notification timer");
3✔
672
        _notification_timer = Timer.periodic(
6✔
673
            Duration(seconds: 5),
3✔
674
                (timer) {
×
675
              _refreshNotifications();
×
676
            });
677
      }
678
    }
679

680
    _connectionStatusChanged();
3✔
681

682
    fetchStatusCodeData();
3✔
683

684
    return _connected;
3✔
685
  }
686

687
  /*
688
   * Request the user roles (permissions) from the InvenTree server
689
   */
690
  Future<bool> _fetchRoles() async {
3✔
691

692
    roles.clear();
6✔
693

694
    debug("API: Requesting user role data");
3✔
695

696
    final response = await get(_URL_ROLES, expectedStatusCode: 200);
3✔
697

698
    if (!response.successful()) {
3✔
699
      return false;
700
    }
701

702
    var data = response.asMap();
3✔
703

704
    if (data.containsKey("roles")) {
3✔
705
      // Save a local copy of the user roles
706
      roles = (response.data["roles"] ?? {}) as Map<String, dynamic>;
9✔
707

708
      return true;
709
    } else {
710
      showServerError(
×
711
        apiUrl,
×
712
        L10().serverError,
×
713
        L10().errorUserRoles,
×
714
      );
715
      return false;
716
    }
717
  }
718

719
  // Request plugin information from the server
720
  Future<bool> _fetchPlugins() async {
3✔
721

722
    _plugins.clear();
6✔
723

724
    debug("API: getPluginInformation()");
3✔
725

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

729
    for (var result in results) {
6✔
730
      if (result is InvenTreePlugin) {
3✔
731
        if (result.active) {
3✔
732
          // Only add plugins that are active
733
          _plugins.add(result);
6✔
734
        }
735
      }
736
    }
737

738
    return true;
739
  }
740

741
  /*
742
   * Check if the user has the given role.permission assigned
743
   * e.g. "part", "change"
744
   */
745
  bool checkPermission(String role, String permission) {
1✔
746

747
    if (!_connected) {
1✔
748
      return false;
749
    }
750

751
    // If we do not have enough information, assume permission is allowed
752
    if (roles.isEmpty) {
2✔
753
      debug("checkPermission - no roles defined!");
×
754
      return true;
755
    }
756

757
    if (!roles.containsKey(role)) {
2✔
758
      debug("checkPermission - role '$role' not found!");
2✔
759
      return true;
760
    }
761

762
    if (roles[role] == null) {
2✔
763
      debug("checkPermission - role '$role' is null!");
×
764
      return false;
765
    }
766

767
    try {
768
      List<String> perms = List.from(roles[role] as List<dynamic>);
3✔
769
      return perms.contains(permission);
1✔
770
    } catch (error, stackTrace) {
771
      if (error is TypeError) {
×
772
        // Ignore TypeError
773
      } else {
774
        // Unknown error - report it!
775
        sentryReportError(
×
776
          "api.checkPermission",
777
          error, stackTrace,
778
          context: {
×
779
            "role": role,
780
            "permission": permission,
781
            "error": error.toString(),
×
782
         }
783
        );
784
      }
785

786
      // Unable to determine permission - assume true?
787
      return true;
788
    }
789
  }
790

791

792
  // Perform a PATCH request
793
  Future<APIResponse> patch(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode}) async {
2✔
794

795
    Map<String, dynamic> _body = body;
796

797
    HttpClientRequest? request = await apiRequest(url, "PATCH");
2✔
798

799
    if (request == null) {
800
      // Return an "invalid" APIResponse
801
      return APIResponse(
×
802
        url: url,
803
        method: "PATCH",
804
        error: "HttpClientRequest is null"
805
      );
806
    }
807

808
    return completeRequest(
2✔
809
      request,
810
      data: json.encode(_body),
2✔
811
      statusCode: expectedStatusCode
812
    );
813
  }
814

815
  /*
816
   * Download a file from the given URL
817
   */
818
  Future<void> downloadFile(String url, {bool openOnDownload = true}) async {
×
819

820
    // Find the local downlods directory
821
    final Directory dir = await getTemporaryDirectory();
×
822

823
    String filename = url.split("/").last;
×
824

825
    String local_path = dir.path + "/" + filename;
×
826

827
    Uri? _uri = Uri.tryParse(makeUrl(url));
×
828

829
    if (_uri == null) {
830
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
831
      return;
832
    }
833

834
    if (_uri.host.isEmpty) {
×
835
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
836
      return;
837
    }
838

839
    HttpClientRequest? _request;
840

841
    final bool strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool;
×
842

843
    var client = createClient(url, strictHttps: strictHttps);
×
844

845
    // Attempt to open a connection to the server
846
    try {
847
      _request = await client.openUrl("GET", _uri).timeout(Duration(seconds: 10));
×
848

849
      // Set headers
850
      defaultHeaders().forEach((key, value) {
×
851
        _request?.headers.set(key, value);
×
852
      });
853

854
    } on SocketException catch (error) {
×
855
      debug("SocketException at ${url}: ${error.toString()}");
×
856
      showServerError(url, L10().connectionRefused, error.toString());
×
857
      return;
858
    } on TimeoutException {
×
859
      debug("TimeoutException at ${url}");
×
860
      showTimeoutError(url);
×
861
      return;
862
    } on HandshakeException catch (error) {
×
863
      debug("HandshakeException at ${url}:");
×
864
      debug(error.toString());
×
865
      showServerError(url, L10().serverCertificateError, error.toString());
×
866
      return;
867
    } catch (error, stackTrace) {
868
      debug("Server error at ${url}: ${error.toString()}");
×
869
      showServerError(url, L10().serverError, error.toString());
×
870
      sentryReportError(
×
871
        "api.downloadFile : client.openUrl",
872
        error, stackTrace,
873
      );
874
      return;
875
    }
876

877
    try {
878
      final response = await _request.close();
×
879

880
      if (response.statusCode == 200) {
×
881
        var bytes = await consolidateHttpClientResponseBytes(response);
×
882

883
        File localFile = File(local_path);
×
884

885
        await localFile.writeAsBytes(bytes);
×
886

887
        if (openOnDownload) {
888
          OpenFilex.open(local_path);
×
889
        }
890
      } else {
891
        showStatusCodeError(url, response.statusCode);
×
892
      }
893
    } on SocketException catch (error) {
×
894
      showServerError(url, L10().connectionRefused, error.toString());
×
895
    } on TimeoutException {
×
896
      showTimeoutError(url);
×
897
    } catch (error, stackTrace) {
898
      debug("Error downloading image:");
×
899
      debug(error.toString());
×
900
      showServerError(url, L10().downloadError, error.toString());
×
901
      sentryReportError(
×
902
        "api.downloadFile : client.closeRequest",
903
        error, stackTrace,
904
      );
905
    }
906
  }
907

908
  /*
909
   * Upload a file to the given URL
910
   */
911
  Future<APIResponse> uploadFile(String url, File f,
×
912
      {String name = "attachment", String method="POST", Map<String, dynamic>? fields}) async {
913
    var _url = makeApiUrl(url);
×
914

915
    var request = http.MultipartRequest(method, Uri.parse(_url));
×
916

917
    request.headers.addAll(defaultHeaders());
×
918

919
    if (fields != null) {
920
      fields.forEach((String key, dynamic value) {
×
921

922
        if (value == null) {
923
          request.fields[key] = "";
×
924
        } else {
925
          request.fields[key] = value.toString();
×
926
        }
927
      });
928
    }
929

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

932
    request.files.add(_file);
×
933

934
    APIResponse response = APIResponse(
×
935
      url: url,
936
      method: method,
937
    );
938

939
    String jsondata = "";
940

941
    try {
942
      var httpResponse = await request.send().timeout(Duration(seconds: 120));
×
943

944
      response.statusCode = httpResponse.statusCode;
×
945

946
      jsondata = await httpResponse.stream.bytesToString();
×
947

948
      response.data = json.decode(jsondata);
×
949

950
      // Report a server-side error
951
      if (response.statusCode == 500) {
×
952
        sentryReportMessage(
×
953
            "Server error in uploadFile()",
954
            context: {
×
955
              "url": url,
956
              "method": request.method,
×
957
              "name": name,
958
              "statusCode": response.statusCode.toString(),
×
959
              "requestHeaders": request.headers.toString(),
×
960
              "responseHeaders": httpResponse.headers.toString(),
×
961
            }
962
        );
963
      }
964
    } on SocketException catch (error) {
×
965
      showServerError(url, L10().connectionRefused, error.toString());
×
966
      response.error = "SocketException";
×
967
      response.errorDetail = error.toString();
×
968
    } on FormatException {
×
969
      showServerError(
×
970
        url,
971
        L10().formatException,
×
972
        L10().formatExceptionJson + ":\n${jsondata}"
×
973
      );
×
974

975
      sentryReportMessage(
×
976
          "Error decoding JSON response from server",
977
          context: {
×
978
            "method": "uploadFile",
979
            "url": url,
980
            "statusCode": response.statusCode.toString(),
×
981
            "data": jsondata,
982
          }
983
      );
984

985
    } on TimeoutException {
×
986
      showTimeoutError(url);
×
987
      response.error = "TimeoutException";
×
988
    } catch (error, stackTrace) {
989
      showServerError(url, L10().serverError, error.toString());
×
990
      sentryReportError(
×
991
        "api.uploadFile",
992
        error, stackTrace
993
      );
994
      response.error = "UnknownError";
×
995
      response.errorDetail = error.toString();
×
996
    }
997

998
    return response;
999
  }
1000

1001
  /*
1002
   * Perform a HTTP OPTIONS request,
1003
   * to get the available fields at a given endpoint.
1004
   * We send this with the currently selected "locale",
1005
   * so that (hopefully) the field messages are correctly translated
1006
   */
1007
  Future<APIResponse> options(String url) async {
×
1008

1009
    HttpClientRequest? request = await apiRequest(url, "OPTIONS");
×
1010

1011
    if (request == null) {
1012
      // Return an "invalid" APIResponse
1013
      return APIResponse(
×
1014
        url: url,
1015
        method: "OPTIONS"
1016
      );
1017
    }
1018

1019
    return completeRequest(request);
×
1020
  }
1021

1022
  /*
1023
   * Perform a HTTP POST request
1024
   * Returns a json object (or null if unsuccessful)
1025
   */
1026
  Future<APIResponse> post(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode=201}) async {
2✔
1027

1028
    HttpClientRequest? request = await apiRequest(url, "POST");
2✔
1029

1030
    if (request == null) {
1031
      // Return an "invalid" APIResponse
1032
      return APIResponse(
1✔
1033
        url: url,
1034
        method: "POST"
1035
      );
1036
    }
1037

1038
    return completeRequest(
1✔
1039
      request,
1040
      data: json.encode(body),
1✔
1041
      statusCode: expectedStatusCode
1042
    );
1043
  }
1044

1045
  /*
1046
   * Perform a request to link a custom barcode to a particular item
1047
   */
1048
  Future<bool> linkBarcode(Map<String, String> body) async {
1✔
1049

1050
  HttpClientRequest? request = await apiRequest("/barcode/link/", "POST");
1✔
1051

1052
  if (request == null) {
1053
    return false;
1054
  }
1055

1056
  final response = await completeRequest(
1✔
1057
    request,
1058
    data: json.encode(body),
1✔
1059
    statusCode: 200
1060
  );
1061

1062
  return response.isValid() && response.statusCode == 200;
3✔
1063

1064
  }
1065

1066
  /*
1067
   * Perform a request to unlink a custom barcode from a particular item
1068
   */
1069
  Future<bool> unlinkBarcode(Map<String, dynamic> body) async {
1✔
1070

1071
    HttpClientRequest? request = await apiRequest("/barcode/unlink/", "POST");
1✔
1072

1073
    if (request == null) {
1074
      return false;
1075
    }
1076

1077
    final response = await completeRequest(
1✔
1078
        request,
1079
        data: json.encode(body),
1✔
1080
        statusCode: 200,
1081
    );
1082

1083
    return response.isValid() && response.statusCode == 200;
3✔
1084
  }
1085

1086

1087
  HttpClient createClient(String url, {bool strictHttps = false}) {
3✔
1088

1089
    var client = HttpClient();
3✔
1090

1091
    client.badCertificateCallback = (X509Certificate cert, String host, int port) {
3✔
1092

1093
      if (strictHttps) {
1094
        showServerError(
×
1095
          url,
1096
          L10().serverCertificateError,
×
1097
          L10().serverCertificateInvalid,
×
1098
        );
1099
        return false;
1100
      }
1101

1102
      // Strict HTTPs not enforced, so we'll ignore the bad cert
1103
      return true;
1104
    };
1105

1106
    // Set the connection timeout
1107
    client.connectionTimeout = Duration(seconds: 30);
6✔
1108

1109
    return client;
1110
  }
1111

1112
  /*
1113
   * Initiate a HTTP request to the server
1114
   *
1115
   * @param url is the API endpoint
1116
   * @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
1117
   * @param params is the request parameters
1118
   */
1119
  Future<HttpClientRequest?> apiRequest(
4✔
1120
      String url,
1121
      String method,
1122
      {
1123
        Map<String, String> urlParams = const {},
1124
        Map<String, String> headers = const {},
1125
      }
1126
    ) async {
1127

1128
    var _url = makeApiUrl(url);
4✔
1129

1130
    // Add any required query parameters to the URL using ?key=value notation
1131
    if (urlParams.isNotEmpty) {
4✔
1132
      String query = "?";
1133

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

1136
      _url += query;
3✔
1137
    }
1138

1139
    // Remove extraneous character if present
1140
    if (_url.endsWith("&")) {
4✔
1141
      _url = _url.substring(0, _url.length - 1);
9✔
1142
    }
1143

1144
    Uri? _uri = Uri.tryParse(_url);
4✔
1145

1146
    if (_uri == null) {
1147
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
1148
      return null;
1149
    }
1150

1151
    if (_uri.host.isEmpty) {
8✔
1152
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
5✔
1153
      return null;
1154
    }
1155

1156
    HttpClientRequest? _request;
1157

1158
    final bool strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool;
6✔
1159

1160
    var client = createClient(url, strictHttps: strictHttps);
3✔
1161

1162
    // Attempt to open a connection to the server
1163
    try {
1164
      _request = await client.openUrl(method, _uri).timeout(Duration(seconds: 10));
9✔
1165

1166
      // Default headers
1167
      defaultHeaders().forEach((key, value) {
9✔
1168
        _request?.headers.set(key, value);
6✔
1169
      });
1170

1171
      // Custom headers
1172
      headers.forEach((key, value) {
5✔
1173
        _request?.headers.set(key, value);
4✔
1174
      });
1175

1176
      return _request;
1177
    } on SocketException catch (error) {
1✔
1178
      debug("SocketException at ${url}: ${error.toString()}");
3✔
1179
      showServerError(url, L10().connectionRefused, error.toString());
4✔
1180
      return null;
1181
    } on TimeoutException {
×
1182
      debug("TimeoutException at ${url}");
×
1183
      showTimeoutError(url);
×
1184
      return null;
1185
    } on OSError catch (error) {
×
1186
      debug("OSError at ${url}: ${error.toString()}");
×
1187
      showServerError(url, L10().connectionRefused, error.toString());
×
1188
      return null;
1189
    } on CertificateException catch (error) {
×
1190
      debug("CertificateException at ${url}:");
×
1191
      debug(error.toString());
×
1192
      showServerError(url, L10().serverCertificateError, error.toString());
×
1193
      return null;
1194
    } on HandshakeException catch (error) {
×
1195
      debug("HandshakeException at ${url}:");
×
1196
      debug(error.toString());
×
1197
      showServerError(url, L10().serverCertificateError, error.toString());
×
1198
      return null;
1199
    } catch (error, stackTrace) {
1200
      debug("Server error at ${url}: ${error.toString()}");
×
1201
      showServerError(url, L10().serverError, error.toString());
×
1202
      sentryReportError(
×
1203
        "api.apiRequest : openUrl",
1204
        error, stackTrace,
1205
        context: {
×
1206
          "url": url,
1207
          "method": method,
1208
        }
1209
      );
1210

1211
      return null;
1212
    }
1213
  }
1214

1215

1216
  /*
1217
   * Complete an API request, and return an APIResponse object
1218
   */
1219
  Future<APIResponse> completeRequest(HttpClientRequest request, {String? data, int? statusCode, bool ignoreResponse = false}) async {
3✔
1220

1221
    if (data != null && data.isNotEmpty) {
2✔
1222

1223
      var encoded_data = utf8.encode(data);
2✔
1224

1225
      request.headers.set(HttpHeaders.contentLengthHeader, encoded_data.length.toString());
8✔
1226
      request.add(encoded_data);
2✔
1227
    }
1228

1229
    APIResponse response = APIResponse(
3✔
1230
      method: request.method,
3✔
1231
      url: request.uri.toString()
6✔
1232
    );
1233

1234
    String url = request.uri.toString();
6✔
1235

1236
    try {
1237
      HttpClientResponse? _response = await request.close().timeout(Duration(seconds: 10));
9✔
1238

1239
      response.statusCode = _response.statusCode;
6✔
1240

1241
      // If the server returns a server error code, alert the user
1242
      if (_response.statusCode >= 500) {
6✔
1243
        showStatusCodeError(url, _response.statusCode);
×
1244

1245
        // Some server errors are not ones for us to worry about!
1246
        switch (_response.statusCode) {
×
1247
          case 502:   // Bad gateway
×
1248
          case 503:   // Service unavailable
×
1249
          case 504:   // Gateway timeout
×
1250
            break;
1251
          default:    // Any other error code
1252
            sentryReportMessage(
×
1253
                "Server error",
1254
                context: {
×
1255
                  "url": request.uri.toString(),
×
1256
                  "method": request.method,
×
1257
                  "statusCode": _response.statusCode.toString(),
×
1258
                  "requestHeaders": request.headers.toString(),
×
1259
                  "responseHeaders": _response.headers.toString(),
×
1260
                  "responseData": response.data.toString(),
×
1261
                }
1262
            );
1263
            break;
1264
        }
1265
      } else {
1266

1267
        response.data = ignoreResponse ? {} : await responseToJson(url, _response) ?? {};
6✔
1268

1269
        // First check that the returned status code is what we expected
1270
        if (statusCode != null && statusCode != _response.statusCode) {
4✔
1271
          showStatusCodeError(url, _response.statusCode, details: response.data.toString());
×
1272
        }
1273
      }
1274
    } on HttpException catch (error) {
×
1275
      showServerError(url, L10().serverError, error.toString());
×
1276
      response.error = "HTTPException";
×
1277
      response.errorDetail = error.toString();
×
1278
    } on SocketException catch (error) {
×
1279
      showServerError(url, L10().connectionRefused, error.toString());
×
1280
      response.error = "SocketException";
×
1281
      response.errorDetail = error.toString();
×
1282
    } on CertificateException catch (error) {
×
1283
      debug("CertificateException at ${request.uri.toString()}:");
×
1284
      debug(error.toString());
×
1285
      showServerError(url, L10().serverCertificateError, error.toString());
×
1286
    } on TimeoutException {
×
1287
      showTimeoutError(url);
×
1288
      response.error = "TimeoutException";
×
1289
    } catch (error, stackTrace) {
1290
      showServerError(url, L10().serverError, error.toString());
×
1291
      sentryReportError("api.completeRequest", error, stackTrace);
×
1292
      response.error = "UnknownError";
×
1293
      response.errorDetail = error.toString();
×
1294
    }
1295

1296
    return response;
1297

1298
  }
1299

1300
  /*
1301
   * Convert a HttpClientResponse response object to JSON
1302
   */
1303
  dynamic responseToJson(String url, HttpClientResponse response) async {
3✔
1304

1305
    String body = await response.transform(utf8.decoder).join();
9✔
1306

1307
    try {
1308
      var data = json.decode(body);
3✔
1309

1310
      return data ?? {};
×
1311
    } on FormatException {
×
1312

1313
      switch (response.statusCode) {
×
1314
        case 400:
×
1315
        case 401:
×
1316
        case 403:
×
1317
        case 404:
×
1318
          // Ignore for unauthorized pages
1319
          break;
1320
        case 502:
×
1321
        case 503:
×
1322
        case 504:
×
1323
          // Ignore for server errors
1324
          break;
1325
        default:
1326
          sentryReportMessage(
×
1327
              "Error decoding JSON response from server",
1328
              context: {
×
1329
                "headers": response.headers.toString(),
×
1330
                "statusCode": response.statusCode.toString(),
×
1331
                "data": body.toString(),
×
1332
                "endpoint": url,
1333
              }
1334
          );
1335
          break;
1336
      }
1337

1338
      showServerError(
×
1339
        url,
1340
        L10().formatException,
×
1341
        L10().formatExceptionJson + ":\n${body}"
×
1342
      );
×
1343

1344
      // Return an empty map
1345
      return {};
×
1346
    }
1347

1348
  }
1349

1350
  /*
1351
   * Perform a HTTP GET request
1352
   * Returns a json object (or null if did not complete)
1353
   */
1354
  Future<APIResponse> get(String url, {Map<String, String> params = const {}, Map<String, String> headers = const {}, int? expectedStatusCode=200}) async {
3✔
1355

1356
    HttpClientRequest? request = await apiRequest(
3✔
1357
      url,
1358
      "GET",
1359
      urlParams: params,
1360
      headers: headers,
1361
    );
1362

1363

1364
    if (request == null) {
1365
      // Return an "invalid" APIResponse
1366
      return APIResponse(
1✔
1367
        url: url,
1368
        method: "GET",
1369
        error: "HttpClientRequest is null",
1370
      );
1371
    }
1372

1373
    return completeRequest(request);
3✔
1374
  }
1375

1376
  /*
1377
   * Perform a HTTP DELETE request
1378
   */
1379
  Future<APIResponse> delete(String url) async {
×
1380

1381
    HttpClientRequest? request = await apiRequest(
×
1382
      url,
1383
      "DELETE",
1384
    );
1385

1386
    if (request == null) {
1387
      // Return an "invalid" APIResponse object
1388
      return APIResponse(
×
1389
        url: url,
1390
        method: "DELETE",
1391
        error: "HttpClientRequest is null",
1392
      );
1393
    }
1394

1395
    return completeRequest(
×
1396
      request,
1397
      ignoreResponse: true,
1398
    );
1399
  }
1400

1401
  // Find the current locale code for the running app
1402
  String get currentLocale {
3✔
1403

1404
    if (hasContext()) {
3✔
1405
      // Try to get app context
1406
      BuildContext? context = OneContext().context;
×
1407

1408
      if (context != null) {
1409
        Locale? locale = InvenTreeApp
1410
            .of(context)
×
1411
            ?.locale;
×
1412

1413
        if (locale != null) {
1414
          return locale.languageCode; //.toString();
×
1415
        }
1416
      }
1417
    }
1418

1419
    // Fallback value
1420
    return Intl.getCurrentLocale();
3✔
1421
  }
1422

1423
  // Return a list of request headers
1424
  Map<String, String> defaultHeaders() {
3✔
1425
    Map<String, String> headers = {};
3✔
1426

1427
    if (hasToken) {
3✔
1428
      headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
6✔
1429
    }
1430

1431
    headers[HttpHeaders.acceptHeader] = "application/json";
3✔
1432
    headers[HttpHeaders.contentTypeHeader] = "application/json";
3✔
1433
    headers[HttpHeaders.acceptLanguageHeader] = currentLocale;
6✔
1434

1435
    return headers;
1436
  }
1437

1438
  // Construct a token authorization header
1439
  String _authorizationHeader() {
3✔
1440
    if (token.isNotEmpty) {
6✔
1441
      return "Token ${token}";
6✔
1442
    } else {
1443
      return "";
1444
    }
1445
  }
1446

1447
  static String get staticImage => "/static/img/blank_image.png";
×
1448

1449
  static String get staticThumb => "/static/img/blank_image.thumbnail.png";
×
1450

1451
  CachedNetworkImage? getThumbnail(String imageUrl, {double size = 40, bool hideIfNull = false}) {
×
1452

1453
    if (hideIfNull) {
1454
      if (imageUrl.isEmpty) {
×
1455
        return null;
1456
      }
1457
    }
1458

1459
    try {
1460
      return getImage(
×
1461
          imageUrl,
1462
          width: size,
1463
          height: size
1464
      );
1465
    } catch (error, stackTrace) {
1466
      sentryReportError("_getThumbnail", error, stackTrace);
×
1467
      return null;
1468
    }
1469
  }
1470

1471
  /*
1472
   * Load image from the InvenTree server,
1473
   * or from local cache (if it has been cached!)
1474
   */
1475
  CachedNetworkImage getImage(String imageUrl, {double? height, double? width}) {
×
1476
    if (imageUrl.isEmpty) {
×
1477
      imageUrl = staticImage;
×
1478
    }
1479

1480
    String url = makeUrl(imageUrl);
×
1481

1482
    const key = "inventree_network_image";
1483

1484
    CacheManager manager = CacheManager(
×
1485
      Config(
×
1486
        key,
1487
        fileService: InvenTreeFileService(
×
1488
          strictHttps: _strictHttps,
×
1489
        ),
1490
      )
1491
    );
1492

1493
    return CachedNetworkImage(
×
1494
      imageUrl: url,
1495
      placeholder: (context, url) => CircularProgressIndicator(),
×
1496
      errorWidget: (context, url, error) => Icon(TablerIcons.circle_x, color: COLOR_DANGER),
×
1497
      httpHeaders: defaultHeaders(),
×
1498
      height: height,
1499
      width: width,
1500
      cacheManager: manager,
1501
    );
1502
  }
1503

1504
  // Keep a record of which settings we have received from the server
1505
  Map<String, InvenTreeGlobalSetting> _globalSettings = {};
1506
  Map<String, InvenTreeUserSetting> _userSettings = {};
1507

1508
  Future<String> getGlobalSetting(String key) async {
×
1509

1510
    InvenTreeGlobalSetting? setting = _globalSettings[key];
×
1511

1512
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1513
      return setting.value;
×
1514
    }
1515

1516
    final response = await InvenTreeGlobalSetting().getModel(key);
×
1517

1518
    if (response is InvenTreeGlobalSetting) {
×
1519
      response.lastReload = DateTime.now();
×
1520
      _globalSettings[key] = response;
×
1521
      return response.value;
×
1522
    } else {
1523
      return "";
1524
    }
1525
  }
1526

1527
  // Return a boolean global setting value
1528
  Future<bool> getGlobalBooleanSetting(String key) async {
×
1529
    String value = await getGlobalSetting(key);
×
1530
    return value.toLowerCase() == "true";
×
1531
  }
1532

1533
  Future<String> getUserSetting(String key) async {
×
1534

1535
    InvenTreeUserSetting? setting = _userSettings[key];
×
1536

1537
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1538
      return setting.value;
×
1539
    }
1540

1541
    final response = await InvenTreeGlobalSetting().getModel(key);
×
1542

1543
    if (response is InvenTreeUserSetting) {
×
1544
      response.lastReload = DateTime.now();
×
1545
      _userSettings[key] = response;
×
1546
      return response.value;
×
1547
    } else {
1548
      return "";
1549
    }
1550
  }
1551

1552
  // Return a boolean user setting value
1553
  Future<bool> getUserBooleanSetting(String key) async {
×
1554
    String value = await getUserSetting(key);
×
1555
    return value.toLowerCase() == "true";
×
1556
  }
1557

1558
  /*
1559
   * Send a request to the server to locate / identify either a StockItem or StockLocation
1560
   */
1561
  Future<void> locateItemOrLocation(BuildContext context, {int? item, int? location}) async {
×
1562

1563
    var plugins = getPlugins(mixin: "locate");
×
1564

1565
    if (plugins.isEmpty) {
×
1566
      // TODO: Error message
1567
      return;
1568
    }
1569

1570
    String plugin_name = "";
1571

1572
    if (plugins.length == 1) {
×
1573
      plugin_name = plugins.first.key;
×
1574
    } else {
1575
      // User selects which plugin to use
1576
      List<Map<String, dynamic>> plugin_options = [];
×
1577

1578
      for (var plugin in plugins) {
×
1579
        plugin_options.add({
×
1580
          "display_name": plugin.humanName,
×
1581
          "value": plugin.key,
×
1582
        });
1583
      }
1584

1585
      Map<String, dynamic> fields = {
×
1586
        "plugin": {
×
1587
          "label": L10().plugin,
×
1588
          "type": "choice",
1589
          "value": plugins.first.key,
×
1590
          "choices": plugin_options,
1591
          "required": true,
1592
        }
1593
      };
1594

1595
      await launchApiForm(
×
1596
          context,
1597
          L10().locateLocation,
×
1598
          "",
1599
          fields,
1600
          icon: TablerIcons.location_search,
1601
          onSuccess: (Map<String, dynamic> data) async {
×
1602
            plugin_name = (data["plugin"] ?? "") as String;
×
1603
          }
1604
      );
1605
    }
1606

1607
    Map<String, dynamic> body = {
×
1608
      "plugin": plugin_name,
1609
    };
1610

1611
    if (item != null) {
1612
      body["item"] = item.toString();
×
1613
    }
1614

1615
    if (location != null) {
1616
      body["location"] = location.toString();
×
1617
    }
1618

1619
    post(
×
1620
      "/api/locate/",
1621
      body: body,
1622
      expectedStatusCode: 200,
1623
    ).then((APIResponse response) {
×
1624
      if (response.successful()) {
×
1625
        showSnackIcon(
×
1626
          L10().requestSuccessful,
×
1627
          success: true,
1628
        );
1629
      }
1630
    });
1631
  }
1632

1633
  // Keep an internal map of status codes
1634
  Map<String, InvenTreeStatusCode> _status_codes = {};
1635

1636
  // Return a status class based on provided URL
1637
  InvenTreeStatusCode _get_status_class(String url) {
3✔
1638
    if (!_status_codes.containsKey(url)) {
6✔
1639
      _status_codes[url] = InvenTreeStatusCode(url);
9✔
1640
    }
1641

1642
    return _status_codes[url]!;
6✔
1643
  }
1644

1645
  // Accessors methods for various status code classes
1646
  InvenTreeStatusCode get StockHistoryStatus => _get_status_class("stock/track/status/");
6✔
1647
  InvenTreeStatusCode get StockStatus => _get_status_class("stock/status/");
6✔
1648
  InvenTreeStatusCode get PurchaseOrderStatus => _get_status_class("order/po/status/");
6✔
1649
  InvenTreeStatusCode get SalesOrderStatus => _get_status_class("order/so/status/");
6✔
1650

1651
  void clearStatusCodeData() {
×
1652
    StockHistoryStatus.data.clear();
×
1653
    StockStatus.data.clear();
×
1654
    PurchaseOrderStatus.data.clear();
×
1655
    SalesOrderStatus.data.clear();
×
1656
  }
1657

1658
  Future<void> fetchStatusCodeData({bool forceReload = true}) async {
3✔
1659
    StockHistoryStatus.load(forceReload: forceReload);
6✔
1660
    StockStatus.load(forceReload: forceReload);
6✔
1661
    PurchaseOrderStatus.load(forceReload: forceReload);
6✔
1662
    SalesOrderStatus.load(forceReload: forceReload);
6✔
1663
  }
1664

1665
  int notification_counter = 0;
1666

1667
  Timer? _notification_timer;
1668

1669
  /*
1670
   * Update notification counter (called periodically)
1671
   */
1672
  Future<void> _refreshNotifications() async {
×
1673
    if (!isConnected()) {
×
1674
      return;
1675
    }
1676

1677
    InvenTreeNotification().count(filters: {"read": "false"}).then((int n) {
×
1678
      notification_counter = n;
×
1679
    });
1680
  }
1681
}
1682

1683

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