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

inventree / inventree-app / 19921246168

04 Dec 2025 07:34AM UTC coverage: 1.464% (-0.003%) from 1.467%
19921246168

push

github

web-flow
Parameters refactor (#738)

* refactor attachment code into its own file

* Add getters

* Remove custom models for each type of attachment

* Refactor existing widgets

* Fix double camera open bug

* Add check for modern parameter API

* Add generic parameter type

* Remove old code

* Remove dead code

* Refactor previous widget

* Remove unused imports

* Refactor common code

* format

* Update release notes

* Helper func to render parameters list tile

* Display parameters on part page

* parameters for company

* Supplier more model types:

- ManufacturerPart
- SupplierPart
- PurchaseOrder
- SalesOrder

* dart format

* Fix image prefix

* Remove unused import

* Adjust API version

0 of 162 new or added lines in 9 files covered. (0.0%)

4 existing lines in 4 files now uncovered.

768 of 52456 relevant lines covered (1.46%)

0.05 hits per line

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

41.27
/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:http/io_client.dart";
8
import "package:intl/intl.dart";
9
import "package:inventree/main.dart";
10
import "package:inventree/widget/progress.dart";
11
import "package:one_context/one_context.dart";
12
import "package:open_filex/open_filex.dart";
13
import "package:cached_network_image/cached_network_image.dart";
14
import "package:flutter/material.dart";
15
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
16
import "package:flutter_cache_manager/flutter_cache_manager.dart";
17
import "package:path_provider/path_provider.dart";
18

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

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

44
  int statusCode = -1;
45

46
  String url = "";
47

48
  String method = "";
49

50
  String error = "";
51

52
  String errorDetail = "";
53

54
  dynamic data = {};
55

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

59
  bool successful() => (statusCode >= 200) && (statusCode < 300);
15✔
60

61
  bool redirected() => (statusCode >= 300) && (statusCode < 400);
×
62

63
  bool clientError() => (statusCode >= 400) && (statusCode < 500);
×
64

65
  bool serverError() => statusCode >= 500;
×
66

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

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

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

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

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

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

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

128
  HttpClient? _client;
129

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

137
    final HttpClientRequest req = await _client!.getUrl(resolved);
×
138

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

145
    final HttpClientResponse httpResponse = await req.close();
×
146

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

157
    return HttpGetResponse(_response);
×
158
  }
159
}
160

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

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

176
  InvenTreeAPI._internal();
4✔
177

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

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

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

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

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

198
  bool _strictHttps = false;
199

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

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

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

213
    return url;
214
  }
215

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

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

225
    return baseUrl + url;
8✔
226
  }
227

228
  String get apiUrl => _makeUrl("/api/");
6✔
229

230
  String get imageUrl => _makeUrl("/image/");
×
231

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

240
  String makeUrl(String endpoint) => _makeUrl(endpoint);
×
241

242
  UserProfile? profile;
243

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

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

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

253
  bool get hasToken => token.isNotEmpty;
9✔
254

255
  String? get serverAddress {
×
256
    return profile?.server;
×
257
  }
258

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

272
      return false;
273
    }
274

275
    // Finally
276
    return true;
277
  }
278

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

282
  String get username => (userInfo["username"] ?? "") as String;
×
283

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

343
  bool get supportsUserPermissions => apiVersion >= 207;
9✔
344

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

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

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

356
  // Does the server support the "modern" (consolidated) parameter API?
357
  // Ref: https://github.com/inventree/InvenTree/pull/10699
NEW
358
  bool get supportsModernParameters => apiVersion >= 429;
×
359

360
  // Cached list of plugins (refreshed when we connect to the server)
361
  List<InvenTreePlugin> _plugins = [];
362

363
  // Return a list of plugins enabled on the server
364
  // Can optionally filter by a particular 'mixin' type
365
  List<InvenTreePlugin> getPlugins({String mixin = ""}) {
×
366
    List<InvenTreePlugin> plugins = [];
×
367

368
    for (var plugin in _plugins) {
×
369
      // Do we wish to filter by a particular mixin?
370
      if (mixin.isNotEmpty) {
×
371
        if (!plugin.supportsMixin(mixin)) {
×
372
          continue;
373
        }
374
      }
375

376
      plugins.add(plugin);
×
377
    }
378

379
    // Return list of matching plugins
380
    return plugins;
381
  }
382

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

386
  // Connection status flag - set once connection has been validated
387
  bool _connected = false;
388

389
  bool _connecting = false;
390

391
  bool isConnected() {
3✔
392
    return profile != null && _connected && baseUrl.isNotEmpty && hasToken;
9✔
393
  }
394

395
  bool isConnecting() {
1✔
396
    return !isConnected() && _connecting;
2✔
397
  }
398

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

417
    if (!hasToken) {
3✔
418
      return false;
419
    }
420

421
    if (!await _checkAuth()) {
3✔
422
      showServerError(
×
423
        _URL_ME,
424
        L10().serverNotConnected,
×
425
        L10().serverAuthenticationError,
×
426
      );
427

428
      // Invalidate the token
429
      if (profile != null) {
×
430
        profile!.token = "";
×
431
        await UserProfileDBManager().updateProfile(profile!);
×
432
      }
433

434
      return false;
435
    }
436

437
    if (!await _fetchRoles()) {
3✔
438
      return false;
439
    }
440

441
    if (!await _fetchPlugins()) {
3✔
442
      return false;
443
    }
444

445
    // Finally, connected
446
    return true;
447
  }
448

449
  /*
450
   * Check that the remote server is available.
451
   * Ping the api/ endpoint, which does not require user authentication
452
   */
453
  Future<bool> _checkServer() async {
3✔
454
    String address = profile?.server ?? "";
6✔
455

456
    if (address.isEmpty) {
3✔
457
      showSnackIcon(
×
458
        L10().incompleteDetails,
×
459
        icon: TablerIcons.exclamation_circle,
460
        success: false,
461
      );
462
      return false;
463
    }
464

465
    if (!address.endsWith("/")) {
3✔
466
      address = address + "/";
1✔
467
    }
468

469
    // Cache the "strictHttps" setting, so we can use it later without async requirement
470
    _strictHttps =
3✔
471
        await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
6✔
472
            as bool;
473

474
    debug("Connecting to ${apiUrl}");
9✔
475

476
    APIResponse response = await get("", expectedStatusCode: 200);
3✔
477

478
    if (!response.successful()) {
3✔
479
      debug("Server returned invalid response: ${response.statusCode}");
3✔
480
      showStatusCodeError(
1✔
481
        apiUrl,
1✔
482
        response.statusCode,
1✔
483
        details: response.data.toString(),
2✔
484
      );
485
      return false;
486
    }
487

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

490
    serverInfo = {..._data};
6✔
491

492
    if (serverVersion.isEmpty) {
6✔
493
      showServerError(apiUrl, L10().missingData, L10().serverMissingData);
×
494

495
      return false;
496
    }
497

498
    if (apiVersion < _minApiVersion) {
6✔
499
      String message = L10().serverApiVersion + ": ${apiVersion}";
×
500

501
      message += "\n";
×
502
      message += L10().serverApiRequired + ": ${_minApiVersion}";
×
503

504
      message += "\n\n";
×
505

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

508
      showServerError(apiUrl, L10().serverOld, message);
×
509

510
      return false;
511
    }
512

513
    // At this point, we have a server which is responding
514
    return true;
515
  }
516

517
  /*
518
   * Check that the user is authenticated
519
   * Fetch the user information
520
   */
521
  Future<bool> _checkAuth() async {
3✔
522
    debug("Checking user auth @ ${_URL_ME}");
3✔
523

524
    userInfo.clear();
6✔
525

526
    final response = await get(_URL_ME);
3✔
527

528
    if (response.successful() && response.statusCode == 200) {
9✔
529
      userInfo = response.asMap();
6✔
530
      return true;
531
    } else {
532
      debug(
×
533
        "Auth request failed: Server returned status ${response.statusCode}",
×
534
      );
535
      if (response.data != null) {
×
536
        debug("Server response: ${response.data.toString()}");
×
537
      }
538

539
      return false;
540
    }
541
  }
542

543
  /*
544
   * Fetch a token from the server,
545
   * with a temporary authentication header
546
   */
547
  Future<APIResponse> fetchToken(
3✔
548
    UserProfile userProfile,
549
    String username,
550
    String password,
551
  ) async {
552
    debug("Fetching user token from ${userProfile.server}");
9✔
553

554
    profile = userProfile;
3✔
555

556
    // Form a name to request the token with
557
    String platform_name = "inventree-mobile-app";
558

559
    final deviceInfo = await getDeviceInfo();
3✔
560

561
    if (Platform.isAndroid) {
3✔
562
      platform_name += "-android";
×
563
    } else if (Platform.isIOS) {
3✔
564
      platform_name += "-ios";
×
565
    } else if (Platform.isMacOS) {
3✔
566
      platform_name += "-macos";
×
567
    } else if (Platform.isLinux) {
3✔
568
      platform_name += "-linux";
3✔
569
    } else if (Platform.isWindows) {
×
570
      platform_name += "-windows";
×
571
    }
572

573
    if (deviceInfo.containsKey("name")) {
3✔
574
      platform_name += "-" + (deviceInfo["name"] as String);
×
575
    }
576

577
    if (deviceInfo.containsKey("model")) {
3✔
578
      platform_name += "-" + (deviceInfo["model"] as String);
×
579
    }
580

581
    if (deviceInfo.containsKey("systemVersion")) {
3✔
582
      platform_name += "-" + (deviceInfo["systemVersion"] as String);
×
583
    }
584

585
    // Construct auth header from username and password
586
    String authHeader =
587
        "Basic " + base64Encode(utf8.encode("${username}:${password}"));
12✔
588

589
    // Perform request to get a token
590
    final response = await get(
3✔
591
      _URL_TOKEN,
592
      params: {"name": platform_name},
3✔
593
      headers: {HttpHeaders.authorizationHeader: authHeader},
3✔
594
    );
595

596
    // Invalid response
597
    if (!response.successful()) {
3✔
598
      switch (response.statusCode) {
1✔
599
        case 401:
1✔
600
        case 403:
×
601
          showServerError(
1✔
602
            apiUrl,
1✔
603
            L10().serverAuthenticationError,
2✔
604
            L10().invalidUsernamePassword,
2✔
605
          );
606
        default:
607
          showStatusCodeError(apiUrl, response.statusCode);
×
608
      }
609

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

612
      if (response.data != null) {
1✔
613
        debug("Response data: ${response.data.toString()}");
4✔
614
      }
615
    }
616

617
    final data = response.asMap();
3✔
618

619
    if (!data.containsKey("token")) {
3✔
620
      showServerError(
1✔
621
        apiUrl,
1✔
622
        L10().tokenMissing,
2✔
623
        L10().tokenMissingFromResponse,
2✔
624
      );
625
    }
626

627
    // Save the token to the user profile
628
    userProfile.token = (data["token"] ?? "") as String;
6✔
629

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

632
    await UserProfileDBManager().updateProfile(userProfile);
6✔
633

634
    return response;
635
  }
636

637
  void disconnectFromServer() {
3✔
638
    debug("API : disconnectFromServer()");
3✔
639

640
    _connected = false;
3✔
641
    _connecting = false;
3✔
642
    profile = null;
3✔
643

644
    // Clear received settings
645
    _globalSettings.clear();
6✔
646
    _userSettings.clear();
6✔
647

648
    roles.clear();
6✔
649
    _plugins.clear();
6✔
650
    serverInfo.clear();
6✔
651
    _connectionStatusChanged();
3✔
652
  }
653

654
  /* Public facing connection function.
655
   */
656
  Future<bool> connectToServer(UserProfile prf) async {
3✔
657
    // Ensure server is first disconnected
658
    disconnectFromServer();
3✔
659

660
    profile = prf;
3✔
661

662
    if (profile == null) {
3✔
663
      showSnackIcon(
×
664
        L10().profileSelect,
×
665
        success: false,
666
        icon: TablerIcons.exclamation_circle,
667
      );
668
      return false;
669
    }
670

671
    // Cancel notification timer
672
    _notification_timer?.cancel();
5✔
673

674
    _connecting = true;
3✔
675
    _connectionStatusChanged();
3✔
676

677
    // Perform the actual connection routine
678
    _connected = await _connectToServer();
6✔
679
    _connecting = false;
3✔
680

681
    if (_connected) {
3✔
682
      showSnackIcon(
3✔
683
        L10().serverConnected,
6✔
684
        icon: TablerIcons.server,
685
        success: true,
686
      );
687

688
      if (_notification_timer == null) {
3✔
689
        debug("starting notification timer");
3✔
690
        _notification_timer = Timer.periodic(Duration(seconds: 60), (timer) {
9✔
691
          _refreshNotifications();
×
692
        });
693
      }
694
    }
695

696
    _connectionStatusChanged();
3✔
697

698
    fetchStatusCodeData();
3✔
699

700
    return _connected;
3✔
701
  }
702

703
  /*
704
   * Request the user roles (permissions) from the InvenTree server
705
   */
706
  Future<bool> _fetchRoles() async {
3✔
707
    roles.clear();
6✔
708

709
    debug("API: Requesting user role data");
3✔
710

711
    final response = await get(_URL_ROLES, expectedStatusCode: 200);
3✔
712

713
    if (!response.successful()) {
3✔
714
      return false;
715
    }
716

717
    var data = response.asMap();
3✔
718

719
    if (!data.containsKey("roles")) {
3✔
720
      roles = {};
×
721
      permissions = {};
×
722

723
      showServerError(apiUrl, L10().serverError, L10().errorUserRoles);
×
724
      return false;
725
    }
726

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

729
    if (supportsUserPermissions && data.containsKey("permissions")) {
6✔
730
      permissions = (data["permissions"] ?? {}) as Map<String, dynamic>;
6✔
731
    } else {
732
      permissions = {};
×
733
    }
734

735
    return true;
736
  }
737

738
  // Request plugin information from the server
739
  Future<bool> _fetchPlugins() async {
3✔
740
    _plugins.clear();
6✔
741

742
    debug("API: getPluginInformation()");
3✔
743

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

747
    for (var result in results) {
6✔
748
      if (result is InvenTreePlugin) {
3✔
749
        if (result.active) {
3✔
750
          // Only add plugins that are active
751
          _plugins.add(result);
6✔
752
        }
753
      }
754
    }
755

756
    return true;
757
  }
758

759
  /*
760
   * Check if the user has the given role.permission assigned
761
   * e.g. "sales_order", "change"
762
   */
763
  bool checkRole(String role, String permission) {
1✔
764
    if (!_connected) {
1✔
765
      return false;
766
    }
767

768
    // If we do not have enough information, assume permission is allowed
769
    if (roles.isEmpty) {
2✔
770
      debug("checkRole - no roles defined!");
×
771
      return true;
772
    }
773

774
    if (!roles.containsKey(role)) {
2✔
775
      debug("checkRole - role '$role' not found!");
2✔
776
      return true;
777
    }
778

779
    if (roles[role] == null) {
2✔
780
      debug("checkRole - role '$role' is null!");
×
781
      return false;
782
    }
783

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

804
      // Unable to determine permission - assume true?
805
      return true;
806
    }
807
  }
808

809
  /*
810
   * Check if the user has the particular model permission assigned
811
   * e.g. "company", "add"
812
   */
813
  bool checkPermission(String model, String permission) {
×
814
    if (!_connected) {
×
815
      return false;
816
    }
817

818
    if (permissions.isEmpty) {
×
819
      // Not enough information available - default to True
820
      return true;
821
    }
822

823
    if (!permissions.containsKey(model)) {
×
824
      debug("checkPermission - model '$model' not found!");
×
825
      return false;
826
    }
827

828
    if (permissions[model] == null) {
×
829
      debug("checkPermission - model '$model' is null!");
×
830
      return false;
831
    }
832

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

853
      // Unable to determine permission - assume true?
854
      return true;
855
    }
856
  }
857

858
  // Perform a PATCH request
859
  Future<APIResponse> patch(
2✔
860
    String url, {
861
    Map<String, dynamic> body = const {},
862
    int? expectedStatusCode,
863
  }) async {
864
    Map<String, dynamic> _body = body;
865

866
    HttpClientRequest? request = await apiRequest(url, "PATCH");
2✔
867

868
    if (request == null) {
869
      // Return an "invalid" APIResponse
870
      return APIResponse(
×
871
        url: url,
872
        method: "PATCH",
873
        error: "HttpClientRequest is null",
874
      );
875
    }
876

877
    return completeRequest(
2✔
878
      request,
879
      data: json.encode(_body),
2✔
880
      statusCode: expectedStatusCode,
881
    );
882
  }
883

884
  /*
885
   * Download a file from the given URL
886
   */
887
  Future<void> downloadFile(String url, {bool openOnDownload = true}) async {
×
888
    if (url.isEmpty) {
×
889
      // No URL provided for download
890
      return;
891
    }
892

893
    // Find the local downloads directory
894
    final Directory dir = await getTemporaryDirectory();
×
895

896
    String filename = url.split("/").last;
×
897

898
    String local_path = dir.path + "/" + filename;
×
899

900
    Uri? _uri = Uri.tryParse(makeUrl(url));
×
901

902
    if (_uri == null) {
903
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
904
      return;
905
    }
906

907
    if (_uri.host.isEmpty) {
×
908
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
909
      return;
910
    }
911

912
    HttpClientRequest? _request;
913

914
    final bool strictHttps =
915
        await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
×
916
            as bool;
917

918
    var client = createClient(url, strictHttps: strictHttps);
×
919

920
    showLoadingOverlay();
×
921

922
    // Attempt to open a connection to the server
923
    try {
924
      _request = await client
925
          .openUrl("GET", _uri)
×
926
          .timeout(Duration(seconds: 10));
×
927

928
      // Set headers
929
      defaultHeaders().forEach((key, value) {
×
930
        _request?.headers.set(key, value);
×
931
      });
932
    } on SocketException catch (error) {
×
933
      debug("SocketException at ${url}: ${error.toString()}");
×
934
      showServerError(url, L10().connectionRefused, error.toString());
×
935
      return;
936
    } on TimeoutException {
×
937
      debug("TimeoutException at ${url}");
×
938
      showTimeoutError(url);
×
939
      return;
940
    } on HandshakeException catch (error) {
×
941
      debug("HandshakeException at ${url}:");
×
942
      debug(error.toString());
×
943
      showServerError(url, L10().serverCertificateError, error.toString());
×
944
      return;
945
    } catch (error, stackTrace) {
946
      debug("Server error at ${url}: ${error.toString()}");
×
947
      showServerError(url, L10().serverError, error.toString());
×
948
      sentryReportError("api.downloadFile : client.openUrl", error, stackTrace);
×
949
      return;
950
    }
951

952
    try {
953
      final response = await _request.close();
×
954

955
      if (response.statusCode == 200) {
×
956
        var bytes = await consolidateHttpClientResponseBytes(response);
×
957

958
        File localFile = File(local_path);
×
959

960
        await localFile.writeAsBytes(bytes);
×
961

962
        if (openOnDownload) {
963
          hideLoadingOverlay();
×
964
          OpenFilex.open(local_path);
×
965
        }
966
      } else {
967
        showStatusCodeError(url, response.statusCode);
×
968
      }
969
    } on SocketException catch (error) {
×
970
      showServerError(url, L10().connectionRefused, error.toString());
×
971
    } on TimeoutException {
×
972
      showTimeoutError(url);
×
973
    } catch (error, stackTrace) {
974
      debug("Error downloading image:");
×
975
      debug(error.toString());
×
976
      showServerError(url, L10().downloadError, error.toString());
×
977
      sentryReportError(
×
978
        "api.downloadFile : client.closeRequest",
979
        error,
980
        stackTrace,
981
      );
982
    }
983

984
    hideLoadingOverlay();
×
985
  }
986

987
  /*
988
   * Upload a file to the given URL
989
   */
990
  Future<APIResponse> uploadFile(
×
991
    String url,
992
    File f, {
993
    String name = "attachment",
994
    String method = "POST",
995
    Map<String, dynamic>? fields,
996
  }) async {
997
    bool strictHttps = await InvenTreeSettingsManager().getBool(
×
998
      INV_STRICT_HTTPS,
999
      false,
1000
    );
1001

1002
    // Create an IOClient wrapper for sending the MultipartRequest
1003
    final ioClient = IOClient(createClient(url, strictHttps: strictHttps));
×
1004

1005
    final uri = Uri.parse(makeApiUrl(url));
×
1006
    final request = http.MultipartRequest(method, uri);
×
1007

1008
    // Default headers
1009
    defaultHeaders().forEach((key, value) {
×
1010
      request.headers[key] = value;
×
1011
    });
1012

1013
    // Optional fields
1014
    if (fields != null) {
1015
      fields.forEach((String key, dynamic value) {
×
1016
        if (value == null) {
1017
          request.fields[key] = "";
×
1018
        } else {
1019
          request.fields[key] = value.toString();
×
1020
        }
1021
      });
1022
    }
1023

1024
    // Add file to upload
1025
    var _file = await http.MultipartFile.fromPath(name, f.path);
×
1026
    request.files.add(_file);
×
1027

1028
    // Construct a response object to return
1029
    APIResponse response = APIResponse(url: url, method: method);
×
1030

1031
    String jsondata = "";
1032

1033
    try {
1034
      var streamedResponse = await ioClient
1035
          .send(request)
×
1036
          .timeout(Duration(seconds: 120));
×
1037
      final httpResponse = await http.Response.fromStream(streamedResponse);
×
1038

1039
      response.statusCode = httpResponse.statusCode;
×
1040

1041
      jsondata = httpResponse.body;
×
1042

1043
      response.data = json.decode(jsondata);
×
1044

1045
      // Report a server-side error
1046
      if (response.statusCode == 500) {
×
1047
        sentryReportMessage(
×
1048
          "Server error in uploadFile()",
1049
          context: {
×
1050
            "url": url,
1051
            "method": request.method,
×
1052
            "name": name,
1053
            "statusCode": response.statusCode.toString(),
×
1054
            "requestHeaders": request.headers.toString(),
×
1055
            "responseHeaders": httpResponse.headers.toString(),
×
1056
          },
1057
        );
1058
      }
1059
    } on SocketException catch (error) {
×
1060
      showServerError(url, L10().connectionRefused, error.toString());
×
1061
      response.error = "SocketException";
×
1062
      response.errorDetail = error.toString();
×
1063
    } on FormatException {
×
1064
      showServerError(
×
1065
        url,
1066
        L10().formatException,
×
1067
        L10().formatExceptionJson + ":\n${jsondata}",
×
1068
      );
1069

1070
      sentryReportMessage(
×
1071
        "Error decoding JSON response from server",
1072
        context: {
×
1073
          "method": "uploadFile",
1074
          "url": url,
1075
          "statusCode": response.statusCode.toString(),
×
1076
          "data": jsondata,
1077
        },
1078
      );
1079
    } on TimeoutException {
×
1080
      showTimeoutError(url);
×
1081
      response.error = "TimeoutException";
×
1082
    } catch (error, stackTrace) {
1083
      showServerError(url, L10().serverError, error.toString());
×
1084
      sentryReportError("api.uploadFile", error, stackTrace);
×
1085
      response.error = "UnknownError";
×
1086
      response.errorDetail = error.toString();
×
1087
    }
1088

1089
    return response;
1090
  }
1091

1092
  /*
1093
   * Perform a HTTP OPTIONS request,
1094
   * to get the available fields at a given endpoint.
1095
   * We send this with the currently selected "locale",
1096
   * so that (hopefully) the field messages are correctly translated
1097
   */
1098
  Future<APIResponse> options(
×
1099
    String url, {
1100
    Map<String, String> params = const {},
1101
  }) async {
1102
    HttpClientRequest? request = await apiRequest(
×
1103
      url,
1104
      "OPTIONS",
1105
      urlParams: params,
1106
    );
1107

1108
    if (request == null) {
1109
      // Return an "invalid" APIResponse
1110
      return APIResponse(url: url, method: "OPTIONS");
×
1111
    }
1112

1113
    return completeRequest(request);
×
1114
  }
1115

1116
  /*
1117
   * Perform a HTTP POST request
1118
   * Returns a json object (or null if unsuccessful)
1119
   */
1120
  Future<APIResponse> post(
2✔
1121
    String url, {
1122
    Map<String, dynamic> body = const {},
1123
    int? expectedStatusCode = 201,
1124
  }) async {
1125
    HttpClientRequest? request = await apiRequest(url, "POST");
2✔
1126

1127
    if (request == null) {
1128
      // Return an "invalid" APIResponse
1129
      return APIResponse(url: url, method: "POST");
1✔
1130
    }
1131

1132
    return completeRequest(
1✔
1133
      request,
1134
      data: json.encode(body),
1✔
1135
      statusCode: expectedStatusCode,
1136
    );
1137
  }
1138

1139
  /*
1140
   * Perform a request to link a custom barcode to a particular item
1141
   */
1142
  Future<bool> linkBarcode(Map<String, String> body) async {
1✔
1143
    HttpClientRequest? request = await apiRequest("/barcode/link/", "POST");
1✔
1144

1145
    if (request == null) {
1146
      return false;
1147
    }
1148

1149
    final response = await completeRequest(
1✔
1150
      request,
1151
      data: json.encode(body),
1✔
1152
      statusCode: 200,
1153
    );
1154

1155
    return response.isValid() && response.statusCode == 200;
3✔
1156
  }
1157

1158
  /*
1159
   * Perform a request to unlink a custom barcode from a particular item
1160
   */
1161
  Future<bool> unlinkBarcode(Map<String, dynamic> body) async {
1✔
1162
    HttpClientRequest? request = await apiRequest("/barcode/unlink/", "POST");
1✔
1163

1164
    if (request == null) {
1165
      return false;
1166
    }
1167

1168
    final response = await completeRequest(
1✔
1169
      request,
1170
      data: json.encode(body),
1✔
1171
      statusCode: 200,
1172
    );
1173

1174
    return response.isValid() && response.statusCode == 200;
3✔
1175
  }
1176

1177
  HttpClient createClient(String url, {bool strictHttps = false}) {
3✔
1178
    var client = HttpClient();
3✔
1179

1180
    client.badCertificateCallback =
3✔
1181
        (X509Certificate cert, String host, int port) {
×
1182
          if (strictHttps) {
1183
            showServerError(
×
1184
              url,
1185
              L10().serverCertificateError,
×
1186
              L10().serverCertificateInvalid,
×
1187
            );
1188
            return false;
1189
          }
1190

1191
          // Strict HTTPs not enforced, so we'll ignore the bad cert
1192
          return true;
1193
        };
1194

1195
    // Set the connection timeout
1196
    client.connectionTimeout = Duration(seconds: 30);
6✔
1197

1198
    return client;
1199
  }
1200

1201
  /*
1202
   * Initiate a HTTP request to the server
1203
   *
1204
   * @param url is the API endpoint
1205
   * @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
1206
   * @param params is the request parameters
1207
   */
1208
  Future<HttpClientRequest?> apiRequest(
4✔
1209
    String url,
1210
    String method, {
1211
    Map<String, String> urlParams = const {},
1212
    Map<String, String> headers = const {},
1213
  }) async {
1214
    var _url = makeApiUrl(url);
4✔
1215

1216
    if (_url.isEmpty) {
4✔
1217
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
1218
      return null;
1219
    }
1220

1221
    // Add any required query parameters to the URL using ?key=value notation
1222
    if (urlParams.isNotEmpty) {
4✔
1223
      String query = "?";
1224

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

1227
      _url += query;
3✔
1228
    }
1229

1230
    // Remove extraneous character if present
1231
    if (_url.endsWith("&")) {
4✔
1232
      _url = _url.substring(0, _url.length - 1);
9✔
1233
    }
1234

1235
    Uri? _uri = Uri.tryParse(_url);
4✔
1236

1237
    if (_uri == null || _uri.host.isEmpty) {
8✔
1238
      showServerError(_url, L10().invalidHost, L10().invalidHostDetails);
5✔
1239
      return null;
1240
    }
1241

1242
    HttpClientRequest? _request;
1243

1244
    final bool strictHttps =
1245
        await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
6✔
1246
            as bool;
1247

1248
    var client = createClient(url, strictHttps: strictHttps);
3✔
1249

1250
    // Attempt to open a connection to the server
1251
    try {
1252
      _request = await client
1253
          .openUrl(method, _uri)
3✔
1254
          .timeout(Duration(seconds: 10));
6✔
1255

1256
      // Default headers
1257
      defaultHeaders().forEach((key, value) {
9✔
1258
        _request?.headers.set(key, value);
6✔
1259
      });
1260

1261
      // Custom headers
1262
      headers.forEach((key, value) {
6✔
1263
        _request?.headers.set(key, value);
6✔
1264
      });
1265

1266
      return _request;
1267
    } on SocketException catch (error) {
1✔
1268
      debug("SocketException at ${url}: ${error.toString()}");
3✔
1269
      showServerError(url, L10().connectionRefused, error.toString());
4✔
1270
      return null;
1271
    } on TimeoutException {
×
1272
      debug("TimeoutException at ${url}");
×
1273
      showTimeoutError(url);
×
1274
      return null;
1275
    } on OSError catch (error) {
×
1276
      debug("OSError at ${url}: ${error.toString()}");
×
1277
      showServerError(url, L10().connectionRefused, error.toString());
×
1278
      return null;
1279
    } on CertificateException catch (error) {
×
1280
      debug("CertificateException at ${url}:");
×
1281
      debug(error.toString());
×
1282
      showServerError(url, L10().serverCertificateError, error.toString());
×
1283
      return null;
1284
    } on HandshakeException catch (error) {
×
1285
      debug("HandshakeException at ${url}:");
×
1286
      debug(error.toString());
×
1287
      showServerError(url, L10().serverCertificateError, error.toString());
×
1288
      return null;
1289
    } catch (error, stackTrace) {
1290
      debug("Server error at ${url}: ${error.toString()}");
×
1291
      showServerError(url, L10().serverError, error.toString());
×
1292
      sentryReportError(
×
1293
        "api.apiRequest : openUrl",
1294
        error,
1295
        stackTrace,
1296
        context: {"url": url, "method": method},
×
1297
      );
1298

1299
      return null;
1300
    }
1301
  }
1302

1303
  /*
1304
   * Complete an API request, and return an APIResponse object
1305
   */
1306
  Future<APIResponse> completeRequest(
3✔
1307
    HttpClientRequest request, {
1308
    String? data,
1309
    int? statusCode,
1310
    bool ignoreResponse = false,
1311
  }) async {
1312
    if (data != null && data.isNotEmpty) {
2✔
1313
      var encoded_data = utf8.encode(data);
2✔
1314

1315
      request.headers.set(
4✔
1316
        HttpHeaders.contentLengthHeader,
1317
        encoded_data.length.toString(),
4✔
1318
      );
1319
      request.add(encoded_data);
2✔
1320
    }
1321

1322
    APIResponse response = APIResponse(
3✔
1323
      method: request.method,
3✔
1324
      url: request.uri.toString(),
6✔
1325
    );
1326

1327
    String url = request.uri.toString();
6✔
1328

1329
    try {
1330
      HttpClientResponse? _response = await request.close().timeout(
6✔
1331
        Duration(seconds: 10),
3✔
1332
      );
1333

1334
      response.statusCode = _response.statusCode;
6✔
1335

1336
      // If the server returns a server error code, alert the user
1337
      if (_response.statusCode >= 500) {
6✔
1338
        showStatusCodeError(url, _response.statusCode);
×
1339

1340
        // Some server errors are not ones for us to worry about!
1341
        switch (_response.statusCode) {
×
1342
          case 502: // Bad gateway
×
1343
          case 503: // Service unavailable
×
1344
          case 504: // Gateway timeout
×
1345
            break;
1346
          default: // Any other error code
1347
            sentryReportMessage(
×
1348
              "Server error",
1349
              context: {
×
1350
                "url": request.uri.toString(),
×
1351
                "method": request.method,
×
1352
                "statusCode": _response.statusCode.toString(),
×
1353
                "requestHeaders": request.headers.toString(),
×
1354
                "responseHeaders": _response.headers.toString(),
×
1355
                "responseData": response.data.toString(),
×
1356
              },
1357
            );
1358
        }
1359
      } else {
1360
        response.data = ignoreResponse
3✔
1361
            ? {}
×
1362
            : await responseToJson(url, _response) ?? {};
3✔
1363

1364
        // First check that the returned status code is what we expected
1365
        if (statusCode != null && statusCode != _response.statusCode) {
4✔
1366
          showStatusCodeError(
×
1367
            url,
1368
            _response.statusCode,
×
1369
            details: response.data.toString(),
×
1370
          );
1371
        }
1372
      }
1373
    } on HttpException catch (error) {
×
1374
      showServerError(url, L10().serverError, error.toString());
×
1375
      response.error = "HTTPException";
×
1376
      response.errorDetail = error.toString();
×
1377
    } on SocketException catch (error) {
×
1378
      showServerError(url, L10().connectionRefused, error.toString());
×
1379
      response.error = "SocketException";
×
1380
      response.errorDetail = error.toString();
×
1381
    } on CertificateException catch (error) {
×
1382
      debug("CertificateException at ${request.uri.toString()}:");
×
1383
      debug(error.toString());
×
1384
      showServerError(url, L10().serverCertificateError, error.toString());
×
1385
    } on TimeoutException {
×
1386
      showTimeoutError(url);
×
1387
      response.error = "TimeoutException";
×
1388
    } catch (error, stackTrace) {
1389
      showServerError(url, L10().serverError, error.toString());
×
1390
      sentryReportError("api.completeRequest", error, stackTrace);
×
1391
      response.error = "UnknownError";
×
1392
      response.errorDetail = error.toString();
×
1393
    }
1394

1395
    return response;
1396
  }
1397

1398
  /*
1399
   * Convert a HttpClientResponse response object to JSON
1400
   */
1401
  dynamic responseToJson(String url, HttpClientResponse response) async {
3✔
1402
    String body = await response.transform(utf8.decoder).join();
9✔
1403

1404
    try {
1405
      var data = json.decode(body);
3✔
1406

1407
      return data ?? {};
×
1408
    } on FormatException {
×
1409
      switch (response.statusCode) {
×
1410
        case 400:
×
1411
        case 401:
×
1412
        case 403:
×
1413
        case 404:
×
1414
          // Ignore for unauthorized pages
1415
          break;
1416
        case 502:
×
1417
        case 503:
×
1418
        case 504:
×
1419
          // Ignore for server errors
1420
          break;
1421
        default:
1422
          sentryReportMessage(
×
1423
            "Error decoding JSON response from server",
1424
            context: {
×
1425
              "headers": response.headers.toString(),
×
1426
              "statusCode": response.statusCode.toString(),
×
1427
              "data": body.toString(),
×
1428
              "endpoint": url,
1429
            },
1430
          );
1431
      }
1432

1433
      showServerError(
×
1434
        url,
1435
        L10().formatException,
×
1436
        L10().formatExceptionJson + ":\n${body}",
×
1437
      );
1438

1439
      // Return an empty map
1440
      return {};
×
1441
    }
1442
  }
1443

1444
  /*
1445
   * Perform a HTTP GET request
1446
   * Returns a json object (or null if did not complete)
1447
   */
1448
  Future<APIResponse> get(
3✔
1449
    String url, {
1450
    Map<String, String> params = const {},
1451
    Map<String, String> headers = const {},
1452
    int? expectedStatusCode = 200,
1453
  }) async {
1454
    HttpClientRequest? request = await apiRequest(
3✔
1455
      url,
1456
      "GET",
1457
      urlParams: params,
1458
      headers: headers,
1459
    );
1460

1461
    if (request == null) {
1462
      // Return an "invalid" APIResponse
1463
      return APIResponse(
1✔
1464
        url: url,
1465
        method: "GET",
1466
        error: "HttpClientRequest is null",
1467
      );
1468
    }
1469

1470
    return completeRequest(request);
3✔
1471
  }
1472

1473
  /*
1474
   * Perform a HTTP DELETE request
1475
   */
1476
  Future<APIResponse> delete(String url) async {
×
1477
    HttpClientRequest? request = await apiRequest(url, "DELETE");
×
1478

1479
    if (request == null) {
1480
      // Return an "invalid" APIResponse object
1481
      return APIResponse(
×
1482
        url: url,
1483
        method: "DELETE",
1484
        error: "HttpClientRequest is null",
1485
      );
1486
    }
1487

1488
    return completeRequest(request, ignoreResponse: true);
×
1489
  }
1490

1491
  // Find the current locale code for the running app
1492
  String get currentLocale {
3✔
1493
    if (hasContext()) {
3✔
1494
      // Try to get app context
1495
      BuildContext? context = OneContext().context;
×
1496

1497
      if (context != null) {
1498
        Locale? locale = InvenTreeApp.of(context)?.locale;
×
1499

1500
        if (locale != null) {
1501
          return locale.languageCode; //.toString();
×
1502
        }
1503
      }
1504
    }
1505

1506
    // Fallback value
1507
    return Intl.getCurrentLocale();
3✔
1508
  }
1509

1510
  // Return a list of request headers
1511
  Map<String, String> defaultHeaders() {
3✔
1512
    Map<String, String> headers = {};
3✔
1513

1514
    if (hasToken) {
3✔
1515
      headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
6✔
1516
    }
1517

1518
    headers[HttpHeaders.acceptHeader] = "application/json";
3✔
1519
    headers[HttpHeaders.contentTypeHeader] = "application/json";
3✔
1520
    headers[HttpHeaders.acceptLanguageHeader] = currentLocale;
6✔
1521

1522
    return headers;
1523
  }
1524

1525
  // Construct a token authorization header
1526
  String _authorizationHeader() {
3✔
1527
    if (token.isNotEmpty) {
6✔
1528
      return "Token ${token}";
6✔
1529
    } else {
1530
      return "";
1531
    }
1532
  }
1533

1534
  static String get staticImage => "/static/img/blank_image.png";
×
1535

1536
  static String get staticThumb => "/static/img/blank_image.thumbnail.png";
×
1537

1538
  CachedNetworkImage? getThumbnail(
×
1539
    String imageUrl, {
1540
    double size = 40,
1541
    bool hideIfNull = false,
1542
  }) {
1543
    if (hideIfNull) {
1544
      if (imageUrl.isEmpty) {
×
1545
        return null;
1546
      }
1547
    }
1548

1549
    try {
1550
      return getImage(imageUrl, width: size, height: size);
×
1551
    } catch (error, stackTrace) {
1552
      sentryReportError("_getThumbnail", error, stackTrace);
×
1553
      return null;
1554
    }
1555
  }
1556

1557
  /*
1558
   * Load image from the InvenTree server,
1559
   * or from local cache (if it has been cached!)
1560
   */
1561
  CachedNetworkImage getImage(
×
1562
    String imageUrl, {
1563
    double? height,
1564
    double? width,
1565
  }) {
1566
    if (imageUrl.isEmpty) {
×
1567
      imageUrl = staticImage;
×
1568
    }
1569

1570
    String url = makeUrl(imageUrl);
×
1571

1572
    const key = "inventree_network_image";
1573

1574
    CacheManager manager = CacheManager(
×
1575
      Config(key, fileService: InvenTreeFileService(strictHttps: _strictHttps)),
×
1576
    );
1577

1578
    return CachedNetworkImage(
×
1579
      imageUrl: url,
1580
      placeholder: (context, url) => CircularProgressIndicator(),
×
1581
      errorWidget: (context, url, error) {
×
1582
        print("CachedNetworkimage error: ${error.toString()}");
×
1583
        return GestureDetector(
×
1584
          child: Icon(TablerIcons.circle_x, color: COLOR_DANGER),
×
1585
          onTap: () => {
×
1586
            showSnackIcon(error.toString().split(",")[0], success: false),
×
1587
          },
1588
        );
1589
      },
1590
      httpHeaders: defaultHeaders(),
×
1591
      height: height,
1592
      width: width,
1593
      cacheManager: manager,
1594
    );
1595
  }
1596

1597
  // Keep a record of which settings we have received from the server
1598
  Map<String, InvenTreeGlobalSetting> _globalSettings = {};
1599
  Map<String, InvenTreeUserSetting> _userSettings = {};
1600

1601
  Future<String> getGlobalSetting(String key) async {
×
1602
    InvenTreeGlobalSetting? setting = _globalSettings[key];
×
1603

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

1608
    final response = await InvenTreeGlobalSetting().getModel(key);
×
1609

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

1619
  // Return a boolean global setting value
1620
  Future<bool> getGlobalBooleanSetting(
×
1621
    String key, {
1622
    bool backup = false,
1623
  }) async {
1624
    String value = await getGlobalSetting(key);
×
1625

1626
    if (value.isEmpty) {
×
1627
      return backup;
1628
    }
1629

1630
    return value.toLowerCase().trim() == "true";
×
1631
  }
1632

1633
  Future<String> getUserSetting(String key) async {
×
1634
    InvenTreeUserSetting? setting = _userSettings[key];
×
1635

1636
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1637
      return setting.value;
×
1638
    }
1639

1640
    final response = await InvenTreeUserSetting().getModel(key);
×
1641

1642
    if (response is InvenTreeUserSetting) {
×
1643
      response.lastReload = DateTime.now();
×
1644
      _userSettings[key] = response;
×
1645
      return response.value;
×
1646
    } else {
1647
      return "";
1648
    }
1649
  }
1650

1651
  // Return a boolean user setting value
1652
  Future<bool> getUserBooleanSetting(String key) async {
×
1653
    String value = await getUserSetting(key);
×
1654
    return value.toLowerCase().trim() == "true";
×
1655
  }
1656

1657
  /*
1658
   * Send a request to the server to locate / identify either a StockItem or StockLocation
1659
   */
1660
  Future<void> locateItemOrLocation(
×
1661
    BuildContext context, {
1662
    int? item,
1663
    int? location,
1664
  }) async {
1665
    var plugins = getPlugins(mixin: "locate");
×
1666

1667
    if (plugins.isEmpty) {
×
1668
      // TODO: Error message
1669
      return;
1670
    }
1671

1672
    String plugin_name = "";
1673

1674
    if (plugins.length == 1) {
×
1675
      plugin_name = plugins.first.key;
×
1676
    } else {
1677
      // User selects which plugin to use
1678
      List<Map<String, dynamic>> plugin_options = [];
×
1679

1680
      for (var plugin in plugins) {
×
1681
        plugin_options.add({
×
1682
          "display_name": plugin.humanName,
×
1683
          "value": plugin.key,
×
1684
        });
1685
      }
1686

1687
      Map<String, dynamic> fields = {
×
1688
        "plugin": {
×
1689
          "label": L10().plugin,
×
1690
          "type": "choice",
1691
          "value": plugins.first.key,
×
1692
          "choices": plugin_options,
1693
          "required": true,
1694
        },
1695
      };
1696

1697
      await launchApiForm(
×
1698
        context,
1699
        L10().locateLocation,
×
1700
        "",
1701
        fields,
1702
        icon: TablerIcons.location_search,
1703
        onSuccess: (Map<String, dynamic> data) async {
×
1704
          plugin_name = (data["plugin"] ?? "") as String;
×
1705
        },
1706
      );
1707
    }
1708

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

1711
    if (item != null) {
1712
      body["item"] = item.toString();
×
1713
    }
1714

1715
    if (location != null) {
1716
      body["location"] = location.toString();
×
1717
    }
1718

1719
    post("/api/locate/", body: body, expectedStatusCode: 200).then((
×
1720
      APIResponse response,
1721
    ) {
1722
      if (response.successful()) {
×
1723
        showSnackIcon(L10().requestSuccessful, success: true);
×
1724
      }
1725
    });
1726
  }
1727

1728
  // Keep an internal map of status codes
1729
  Map<String, InvenTreeStatusCode> _status_codes = {};
1730

1731
  // Return a status class based on provided URL
1732
  InvenTreeStatusCode _get_status_class(String url) {
3✔
1733
    if (!_status_codes.containsKey(url)) {
6✔
1734
      _status_codes[url] = InvenTreeStatusCode(url);
9✔
1735
    }
1736

1737
    return _status_codes[url]!;
6✔
1738
  }
1739

1740
  // Accessors methods for various status code classes
1741
  InvenTreeStatusCode get StockHistoryStatus =>
3✔
1742
      _get_status_class("stock/track/status/");
3✔
1743
  InvenTreeStatusCode get StockStatus => _get_status_class("stock/status/");
6✔
1744
  InvenTreeStatusCode get PurchaseOrderStatus =>
3✔
1745
      _get_status_class("order/po/status/");
3✔
1746
  InvenTreeStatusCode get SalesOrderStatus =>
3✔
1747
      _get_status_class("order/so/status/");
3✔
1748

1749
  void clearStatusCodeData() {
×
1750
    StockHistoryStatus.data.clear();
×
1751
    StockStatus.data.clear();
×
1752
    PurchaseOrderStatus.data.clear();
×
1753
    SalesOrderStatus.data.clear();
×
1754
  }
1755

1756
  Future<void> fetchStatusCodeData({bool forceReload = true}) async {
3✔
1757
    StockHistoryStatus.load(forceReload: forceReload);
6✔
1758
    StockStatus.load(forceReload: forceReload);
6✔
1759
    PurchaseOrderStatus.load(forceReload: forceReload);
6✔
1760
    SalesOrderStatus.load(forceReload: forceReload);
6✔
1761
  }
1762

1763
  int notification_counter = 0;
1764

1765
  Timer? _notification_timer;
1766

1767
  /*
1768
   * Update notification counter (called periodically)
1769
   */
1770
  Future<void> _refreshNotifications() async {
×
1771
    if (!isConnected()) {
×
1772
      return;
1773
    }
1774

1775
    InvenTreeNotification().count(filters: {"read": "false"}).then((int n) {
×
1776
      notification_counter = n;
×
1777
    });
1778
  }
1779
}
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