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

inventree / inventree-app / 12867907395

20 Jan 2025 12:22PM UTC coverage: 8.328% (-0.02%) from 8.351%
12867907395

push

github

web-flow
Order responsible (#602)

* Bump release notes and version

* Display responsible owner for purchase order

* Display responsible owner for sales order

* Display order completion date

0 of 35 new or added lines in 4 files covered. (0.0%)

1 existing line in 1 file now uncovered.

727 of 8730 relevant lines covered (8.33%)

0.29 hits per line

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

41.68
/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
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
    if (url.isEmpty) {
×
821
      // No URL provided for download
822
      return;
823
    }
824

825
    // Find the local downlods directory
826
    final Directory dir = await getTemporaryDirectory();
×
827

828
    String filename = url.split("/").last;
×
829

830
    String local_path = dir.path + "/" + filename;
×
831

832
    Uri? _uri = Uri.tryParse(makeUrl(url));
×
833

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

839
    if (_uri.host.isEmpty) {
×
840
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
841
      return;
842
    }
843

844
    HttpClientRequest? _request;
845

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

848
    var client = createClient(url, strictHttps: strictHttps);
×
849

850
    // Attempt to open a connection to the server
851
    try {
852
      _request = await client.openUrl("GET", _uri).timeout(Duration(seconds: 10));
×
853

854
      // Set headers
855
      defaultHeaders().forEach((key, value) {
×
856
        _request?.headers.set(key, value);
×
857
      });
858

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

882
    try {
883
      final response = await _request.close();
×
884

885
      if (response.statusCode == 200) {
×
886
        var bytes = await consolidateHttpClientResponseBytes(response);
×
887

888
        File localFile = File(local_path);
×
889

890
        await localFile.writeAsBytes(bytes);
×
891

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

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

920
    var request = http.MultipartRequest(method, Uri.parse(_url));
×
921

922
    request.headers.addAll(defaultHeaders());
×
923

924
    if (fields != null) {
925
      fields.forEach((String key, dynamic value) {
×
926

927
        if (value == null) {
928
          request.fields[key] = "";
×
929
        } else {
930
          request.fields[key] = value.toString();
×
931
        }
932
      });
933
    }
934

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

937
    request.files.add(_file);
×
938

939
    APIResponse response = APIResponse(
×
940
      url: url,
941
      method: method,
942
    );
943

944
    String jsondata = "";
945

946
    try {
947
      var httpResponse = await request.send().timeout(Duration(seconds: 120));
×
948

949
      response.statusCode = httpResponse.statusCode;
×
950

951
      jsondata = await httpResponse.stream.bytesToString();
×
952

953
      response.data = json.decode(jsondata);
×
954

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

980
      sentryReportMessage(
×
981
          "Error decoding JSON response from server",
982
          context: {
×
983
            "method": "uploadFile",
984
            "url": url,
985
            "statusCode": response.statusCode.toString(),
×
986
            "data": jsondata,
987
          }
988
      );
989

990
    } on TimeoutException {
×
991
      showTimeoutError(url);
×
992
      response.error = "TimeoutException";
×
993
    } catch (error, stackTrace) {
994
      showServerError(url, L10().serverError, error.toString());
×
995
      sentryReportError(
×
996
        "api.uploadFile",
997
        error, stackTrace
998
      );
999
      response.error = "UnknownError";
×
1000
      response.errorDetail = error.toString();
×
1001
    }
1002

1003
    return response;
1004
  }
1005

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

1014
    HttpClientRequest? request = await apiRequest(url, "OPTIONS");
×
1015

1016
    if (request == null) {
1017
      // Return an "invalid" APIResponse
1018
      return APIResponse(
×
1019
        url: url,
1020
        method: "OPTIONS"
1021
      );
1022
    }
1023

1024
    return completeRequest(request);
×
1025
  }
1026

1027
  /*
1028
   * Perform a HTTP POST request
1029
   * Returns a json object (or null if unsuccessful)
1030
   */
1031
  Future<APIResponse> post(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode=201}) async {
2✔
1032

1033
    HttpClientRequest? request = await apiRequest(url, "POST");
2✔
1034

1035
    if (request == null) {
1036
      // Return an "invalid" APIResponse
1037
      return APIResponse(
1✔
1038
        url: url,
1039
        method: "POST"
1040
      );
1041
    }
1042

1043
    return completeRequest(
1✔
1044
      request,
1045
      data: json.encode(body),
1✔
1046
      statusCode: expectedStatusCode
1047
    );
1048
  }
1049

1050
  /*
1051
   * Perform a request to link a custom barcode to a particular item
1052
   */
1053
  Future<bool> linkBarcode(Map<String, String> body) async {
1✔
1054

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

1057
  if (request == null) {
1058
    return false;
1059
  }
1060

1061
  final response = await completeRequest(
1✔
1062
    request,
1063
    data: json.encode(body),
1✔
1064
    statusCode: 200
1065
  );
1066

1067
  return response.isValid() && response.statusCode == 200;
3✔
1068

1069
  }
1070

1071
  /*
1072
   * Perform a request to unlink a custom barcode from a particular item
1073
   */
1074
  Future<bool> unlinkBarcode(Map<String, dynamic> body) async {
1✔
1075

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

1078
    if (request == null) {
1079
      return false;
1080
    }
1081

1082
    final response = await completeRequest(
1✔
1083
        request,
1084
        data: json.encode(body),
1✔
1085
        statusCode: 200,
1086
    );
1087

1088
    return response.isValid() && response.statusCode == 200;
3✔
1089
  }
1090

1091

1092
  HttpClient createClient(String url, {bool strictHttps = false}) {
3✔
1093

1094
    var client = HttpClient();
3✔
1095

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

1098
      if (strictHttps) {
1099
        showServerError(
×
1100
          url,
1101
          L10().serverCertificateError,
×
1102
          L10().serverCertificateInvalid,
×
1103
        );
1104
        return false;
1105
      }
1106

1107
      // Strict HTTPs not enforced, so we'll ignore the bad cert
1108
      return true;
1109
    };
1110

1111
    // Set the connection timeout
1112
    client.connectionTimeout = Duration(seconds: 30);
6✔
1113

1114
    return client;
1115
  }
1116

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

1133
    var _url = makeApiUrl(url);
4✔
1134

1135
    // Add any required query parameters to the URL using ?key=value notation
1136
    if (urlParams.isNotEmpty) {
4✔
1137
      String query = "?";
1138

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

1141
      _url += query;
3✔
1142
    }
1143

1144
    // Remove extraneous character if present
1145
    if (_url.endsWith("&")) {
4✔
1146
      _url = _url.substring(0, _url.length - 1);
9✔
1147
    }
1148

1149
    Uri? _uri = Uri.tryParse(_url);
4✔
1150

1151
    if (_uri == null) {
1152
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
1153
      return null;
1154
    }
1155

1156
    if (_uri.host.isEmpty) {
8✔
1157
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
5✔
1158
      return null;
1159
    }
1160

1161
    HttpClientRequest? _request;
1162

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

1165
    var client = createClient(url, strictHttps: strictHttps);
3✔
1166

1167
    // Attempt to open a connection to the server
1168
    try {
1169
      _request = await client.openUrl(method, _uri).timeout(Duration(seconds: 10));
9✔
1170

1171
      // Default headers
1172
      defaultHeaders().forEach((key, value) {
9✔
1173
        _request?.headers.set(key, value);
6✔
1174
      });
1175

1176
      // Custom headers
1177
      headers.forEach((key, value) {
5✔
1178
        _request?.headers.set(key, value);
4✔
1179
      });
1180

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

1216
      return null;
1217
    }
1218
  }
1219

1220

1221
  /*
1222
   * Complete an API request, and return an APIResponse object
1223
   */
1224
  Future<APIResponse> completeRequest(HttpClientRequest request, {String? data, int? statusCode, bool ignoreResponse = false}) async {
3✔
1225

1226
    if (data != null && data.isNotEmpty) {
2✔
1227

1228
      var encoded_data = utf8.encode(data);
2✔
1229

1230
      request.headers.set(HttpHeaders.contentLengthHeader, encoded_data.length.toString());
8✔
1231
      request.add(encoded_data);
2✔
1232
    }
1233

1234
    APIResponse response = APIResponse(
3✔
1235
      method: request.method,
3✔
1236
      url: request.uri.toString()
6✔
1237
    );
1238

1239
    String url = request.uri.toString();
6✔
1240

1241
    try {
1242
      HttpClientResponse? _response = await request.close().timeout(Duration(seconds: 10));
9✔
1243

1244
      response.statusCode = _response.statusCode;
6✔
1245

1246
      // If the server returns a server error code, alert the user
1247
      if (_response.statusCode >= 500) {
6✔
1248
        showStatusCodeError(url, _response.statusCode);
×
1249

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

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

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

1301
    return response;
1302

1303
  }
1304

1305
  /*
1306
   * Convert a HttpClientResponse response object to JSON
1307
   */
1308
  dynamic responseToJson(String url, HttpClientResponse response) async {
3✔
1309

1310
    String body = await response.transform(utf8.decoder).join();
9✔
1311

1312
    try {
1313
      var data = json.decode(body);
3✔
1314

1315
      return data ?? {};
×
1316
    } on FormatException {
×
1317

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

1343
      showServerError(
×
1344
        url,
1345
        L10().formatException,
×
1346
        L10().formatExceptionJson + ":\n${body}"
×
1347
      );
×
1348

1349
      // Return an empty map
1350
      return {};
×
1351
    }
1352

1353
  }
1354

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

1361
    HttpClientRequest? request = await apiRequest(
3✔
1362
      url,
1363
      "GET",
1364
      urlParams: params,
1365
      headers: headers,
1366
    );
1367

1368

1369
    if (request == null) {
1370
      // Return an "invalid" APIResponse
1371
      return APIResponse(
1✔
1372
        url: url,
1373
        method: "GET",
1374
        error: "HttpClientRequest is null",
1375
      );
1376
    }
1377

1378
    return completeRequest(request);
3✔
1379
  }
1380

1381
  /*
1382
   * Perform a HTTP DELETE request
1383
   */
1384
  Future<APIResponse> delete(String url) async {
×
1385

1386
    HttpClientRequest? request = await apiRequest(
×
1387
      url,
1388
      "DELETE",
1389
    );
1390

1391
    if (request == null) {
1392
      // Return an "invalid" APIResponse object
1393
      return APIResponse(
×
1394
        url: url,
1395
        method: "DELETE",
1396
        error: "HttpClientRequest is null",
1397
      );
1398
    }
1399

1400
    return completeRequest(
×
1401
      request,
1402
      ignoreResponse: true,
1403
    );
1404
  }
1405

1406
  // Find the current locale code for the running app
1407
  String get currentLocale {
3✔
1408

1409
    if (hasContext()) {
3✔
1410
      // Try to get app context
1411
      BuildContext? context = OneContext().context;
×
1412

1413
      if (context != null) {
1414
        Locale? locale = InvenTreeApp
1415
            .of(context)
×
1416
            ?.locale;
×
1417

1418
        if (locale != null) {
1419
          return locale.languageCode; //.toString();
×
1420
        }
1421
      }
1422
    }
1423

1424
    // Fallback value
1425
    return Intl.getCurrentLocale();
3✔
1426
  }
1427

1428
  // Return a list of request headers
1429
  Map<String, String> defaultHeaders() {
3✔
1430
    Map<String, String> headers = {};
3✔
1431

1432
    if (hasToken) {
3✔
1433
      headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
6✔
1434
    }
1435

1436
    headers[HttpHeaders.acceptHeader] = "application/json";
3✔
1437
    headers[HttpHeaders.contentTypeHeader] = "application/json";
3✔
1438
    headers[HttpHeaders.acceptLanguageHeader] = currentLocale;
6✔
1439

1440
    return headers;
1441
  }
1442

1443
  // Construct a token authorization header
1444
  String _authorizationHeader() {
3✔
1445
    if (token.isNotEmpty) {
6✔
1446
      return "Token ${token}";
6✔
1447
    } else {
1448
      return "";
1449
    }
1450
  }
1451

1452
  static String get staticImage => "/static/img/blank_image.png";
×
1453

1454
  static String get staticThumb => "/static/img/blank_image.thumbnail.png";
×
1455

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

1458
    if (hideIfNull) {
1459
      if (imageUrl.isEmpty) {
×
1460
        return null;
1461
      }
1462
    }
1463

1464
    try {
1465
      return getImage(
×
1466
          imageUrl,
1467
          width: size,
1468
          height: size
1469
      );
1470
    } catch (error, stackTrace) {
1471
      sentryReportError("_getThumbnail", error, stackTrace);
×
1472
      return null;
1473
    }
1474
  }
1475

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

1485
    String url = makeUrl(imageUrl);
×
1486

1487
    const key = "inventree_network_image";
1488

1489
    CacheManager manager = CacheManager(
×
1490
      Config(
×
1491
        key,
1492
        fileService: InvenTreeFileService(
×
1493
          strictHttps: _strictHttps,
×
1494
        ),
1495
      )
1496
    );
1497

1498
    return CachedNetworkImage(
×
1499
      imageUrl: url,
1500
      placeholder: (context, url) => CircularProgressIndicator(),
×
1501
      errorWidget: (context, url, error) => Icon(TablerIcons.circle_x, color: COLOR_DANGER),
×
1502
      httpHeaders: defaultHeaders(),
×
1503
      height: height,
1504
      width: width,
1505
      cacheManager: manager,
1506
    );
1507
  }
1508

1509
  // Keep a record of which settings we have received from the server
1510
  Map<String, InvenTreeGlobalSetting> _globalSettings = {};
1511
  Map<String, InvenTreeUserSetting> _userSettings = {};
1512

1513
  Future<String> getGlobalSetting(String key) async {
×
1514

1515
    InvenTreeGlobalSetting? setting = _globalSettings[key];
×
1516

1517
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1518
      return setting.value;
×
1519
    }
1520

1521
    final response = await InvenTreeGlobalSetting().getModel(key);
×
1522

1523
    if (response is InvenTreeGlobalSetting) {
×
1524
      response.lastReload = DateTime.now();
×
1525
      _globalSettings[key] = response;
×
1526
      return response.value;
×
1527
    } else {
1528
      return "";
1529
    }
1530
  }
1531

1532
  // Return a boolean global setting value
NEW
1533
  Future<bool> getGlobalBooleanSetting(String key, { bool backup = false }) async {
×
1534
    String value = await getGlobalSetting(key);
×
1535

NEW
1536
    if (value.isEmpty) {
×
1537
      return backup;
1538
    }
1539

UNCOV
1540
    return value.toLowerCase().trim() == "true";
×
1541
  }
1542

1543
  Future<String> getUserSetting(String key) async {
×
1544

1545
    InvenTreeUserSetting? setting = _userSettings[key];
×
1546

1547
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1548
      return setting.value;
×
1549
    }
1550

1551
    final response = await InvenTreeUserSetting().getModel(key);
×
1552

1553
    if (response is InvenTreeUserSetting) {
×
1554
      response.lastReload = DateTime.now();
×
1555
      _userSettings[key] = response;
×
1556
      return response.value;
×
1557
    } else {
1558
      return "";
1559
    }
1560
  }
1561

1562
  // Return a boolean user setting value
1563
  Future<bool> getUserBooleanSetting(String key) async {
×
1564
    String value = await getUserSetting(key);
×
1565
    return value.toLowerCase().trim() == "true";
×
1566
  }
1567

1568
  /*
1569
   * Send a request to the server to locate / identify either a StockItem or StockLocation
1570
   */
1571
  Future<void> locateItemOrLocation(BuildContext context, {int? item, int? location}) async {
×
1572

1573
    var plugins = getPlugins(mixin: "locate");
×
1574

1575
    if (plugins.isEmpty) {
×
1576
      // TODO: Error message
1577
      return;
1578
    }
1579

1580
    String plugin_name = "";
1581

1582
    if (plugins.length == 1) {
×
1583
      plugin_name = plugins.first.key;
×
1584
    } else {
1585
      // User selects which plugin to use
1586
      List<Map<String, dynamic>> plugin_options = [];
×
1587

1588
      for (var plugin in plugins) {
×
1589
        plugin_options.add({
×
1590
          "display_name": plugin.humanName,
×
1591
          "value": plugin.key,
×
1592
        });
1593
      }
1594

1595
      Map<String, dynamic> fields = {
×
1596
        "plugin": {
×
1597
          "label": L10().plugin,
×
1598
          "type": "choice",
1599
          "value": plugins.first.key,
×
1600
          "choices": plugin_options,
1601
          "required": true,
1602
        }
1603
      };
1604

1605
      await launchApiForm(
×
1606
          context,
1607
          L10().locateLocation,
×
1608
          "",
1609
          fields,
1610
          icon: TablerIcons.location_search,
1611
          onSuccess: (Map<String, dynamic> data) async {
×
1612
            plugin_name = (data["plugin"] ?? "") as String;
×
1613
          }
1614
      );
1615
    }
1616

1617
    Map<String, dynamic> body = {
×
1618
      "plugin": plugin_name,
1619
    };
1620

1621
    if (item != null) {
1622
      body["item"] = item.toString();
×
1623
    }
1624

1625
    if (location != null) {
1626
      body["location"] = location.toString();
×
1627
    }
1628

1629
    post(
×
1630
      "/api/locate/",
1631
      body: body,
1632
      expectedStatusCode: 200,
1633
    ).then((APIResponse response) {
×
1634
      if (response.successful()) {
×
1635
        showSnackIcon(
×
1636
          L10().requestSuccessful,
×
1637
          success: true,
1638
        );
1639
      }
1640
    });
1641
  }
1642

1643
  // Keep an internal map of status codes
1644
  Map<String, InvenTreeStatusCode> _status_codes = {};
1645

1646
  // Return a status class based on provided URL
1647
  InvenTreeStatusCode _get_status_class(String url) {
3✔
1648
    if (!_status_codes.containsKey(url)) {
6✔
1649
      _status_codes[url] = InvenTreeStatusCode(url);
9✔
1650
    }
1651

1652
    return _status_codes[url]!;
6✔
1653
  }
1654

1655
  // Accessors methods for various status code classes
1656
  InvenTreeStatusCode get StockHistoryStatus => _get_status_class("stock/track/status/");
6✔
1657
  InvenTreeStatusCode get StockStatus => _get_status_class("stock/status/");
6✔
1658
  InvenTreeStatusCode get PurchaseOrderStatus => _get_status_class("order/po/status/");
6✔
1659
  InvenTreeStatusCode get SalesOrderStatus => _get_status_class("order/so/status/");
6✔
1660

1661
  void clearStatusCodeData() {
×
1662
    StockHistoryStatus.data.clear();
×
1663
    StockStatus.data.clear();
×
1664
    PurchaseOrderStatus.data.clear();
×
1665
    SalesOrderStatus.data.clear();
×
1666
  }
1667

1668
  Future<void> fetchStatusCodeData({bool forceReload = true}) async {
3✔
1669
    StockHistoryStatus.load(forceReload: forceReload);
6✔
1670
    StockStatus.load(forceReload: forceReload);
6✔
1671
    PurchaseOrderStatus.load(forceReload: forceReload);
6✔
1672
    SalesOrderStatus.load(forceReload: forceReload);
6✔
1673
  }
1674

1675
  int notification_counter = 0;
1676

1677
  Timer? _notification_timer;
1678

1679
  /*
1680
   * Update notification counter (called periodically)
1681
   */
1682
  Future<void> _refreshNotifications() async {
×
1683
    if (!isConnected()) {
×
1684
      return;
1685
    }
1686

1687
    InvenTreeNotification().count(filters: {"read": "false"}).then((int n) {
×
1688
      notification_counter = n;
×
1689
    });
1690
  }
1691
}
1692

1693

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