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

inventree / inventree-app / 4618591944

pending completion
4618591944

push

github

GitHub
Support barcode scan for purchase order (#298)

15 of 15 new or added lines in 2 files covered. (100.0%)

536 of 6385 relevant lines covered (8.39%)

0.31 hits per line

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

38.48
/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/app_colors.dart";
9
import "package:inventree/preferences.dart";
10

11
import "package:open_filex/open_filex.dart";
12
import "package:cached_network_image/cached_network_image.dart";
13
import "package:flutter/material.dart";
14
import "package:font_awesome_flutter/font_awesome_flutter.dart";
15
import "package:flutter_cache_manager/flutter_cache_manager.dart";
16

17
import "package:inventree/widget/dialogs.dart";
18
import "package:inventree/l10.dart";
19
import "package:inventree/helpers.dart";
20
import "package:inventree/inventree/sentry.dart";
21
import "package:inventree/inventree/model.dart";
22
import "package:inventree/user_profile.dart";
23
import "package:inventree/widget/snacks.dart";
24
import "package:path_provider/path_provider.dart";
25

26
import "package:inventree/api_form.dart";
27

28

29
/*
30
 * Class representing an API response from the server
31
 */
32
class APIResponse {
33

34
  APIResponse({this.url = "", this.method = "", this.statusCode = -1, this.error = "", this.data = const {}});
3✔
35

36
  int statusCode = -1;
37

38
  String url = "";
39

40
  String method = "";
41

42
  String error = "";
43

44
  String errorDetail = "";
45

46
  dynamic data = {};
47

48
  // Request is "valid" if a statusCode was returned
49
  bool isValid() => (statusCode >= 0) && (statusCode < 500);
10✔
50

51
  bool successful() => (statusCode >= 200) && (statusCode < 300);
15✔
52

53
  bool redirected() => (statusCode >= 300) && (statusCode < 400);
×
54

55
  bool clientError() => (statusCode >= 400) && (statusCode < 500);
×
56

57
  bool serverError() => statusCode >= 500;
×
58

59
  bool isMap() {
3✔
60
    return data != null && data is Map<String, dynamic>;
9✔
61
  }
62

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

72
  bool isList() {
1✔
73
    return data != null && data is List<dynamic>;
3✔
74
  }
75

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

85

86
/*
87
 * Custom FileService for caching network images
88
 * Requires a custom badCertificateCallback,
89
 * so we can accept "dodgy" (e.g. self-signed) certificates
90
 */
91
class InvenTreeFileService extends FileService {
92

93
  InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
×
94
    _client = client ?? HttpClient();
×
95

96
    if (_client != null) {
×
97
      _client!.badCertificateCallback = (cert, host, port) {
×
98
        print("BAD CERTIFICATE CALLBACK FOR IMAGE REQUEST");
×
99
        return !strictHttps;
100
      };
101
    }
102
  }
103

104
  HttpClient? _client;
105

106
  @override
×
107
  Future<FileServiceResponse> get(String url,
108
      {Map<String, String>? headers}) async {
109
    final Uri resolved = Uri.base.resolve(url);
×
110

111
    final HttpClientRequest req = await _client!.getUrl(resolved);
×
112

113
    if (headers != null) {
114
      headers.forEach((key, value) {
×
115
        req.headers.add(key, value);
×
116
      });
117
    }
118

119
    final HttpClientResponse httpResponse = await req.close();
×
120

121
    final http.StreamedResponse _response = http.StreamedResponse(
×
122
      httpResponse.timeout(Duration(seconds: 60)), httpResponse.statusCode,
×
123
      contentLength: httpResponse.contentLength < 0 ? 0 : httpResponse.contentLength,
×
124
      reasonPhrase: httpResponse.reasonPhrase,
×
125
      isRedirect: httpResponse.isRedirect,
×
126
    );
127

128
    return HttpGetResponse(_response);
×
129
  }
130
}
131

132
/*
133
 * InvenTree API - Access to the InvenTree REST interface.
134
 *
135
 * InvenTree implements token-based authentication, which is
136
 * initialised using a username:password combination.
137
 */
138

139

140
/*
141
 * API class which manages all communication with the InvenTree server
142
 */
143
class InvenTreeAPI {
144

145
  factory InvenTreeAPI() {
3✔
146
    return _api;
3✔
147
  }
148

149
  InvenTreeAPI._internal();
3✔
150

151
  // List of callback functions to trigger when the connection status changes
152
  List<Function()> _statusCallbacks = [];
153

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

157
  void _connectionStatusChanged() {
3✔
158
    for (Function() func in _statusCallbacks) {
3✔
159
      // Call the function
160
      func();
×
161
    }
162
  }
163

164
  // Minimum required API version for server
165
  static const _minApiVersion = 20;
166

167
  bool _strictHttps = false;
168

169
  // Endpoint for requesting an API token
170
  static const _URL_GET_TOKEN = "user/token/";
171

172
  static const _URL_GET_ROLES = "user/roles/";
173

174
  // Base URL for InvenTree API e.g. http://192.168.120.10:8000
175
  String _BASE_URL = "";
176

177
  // Accessors for various url endpoints
178
  String get baseUrl {
3✔
179
    String url = _BASE_URL;
3✔
180

181
    if (!url.endsWith("/")) {
3✔
182
      url += "/";
×
183
    }
184

185
    return url;
186
  }
187

188
  String _makeUrl(String url) {
3✔
189

190
    // Strip leading slash
191
    if (url.startsWith("/")) {
3✔
192
      url = url.substring(1, url.length);
6✔
193
    }
194

195
    // Prevent double-slash
196
    url = url.replaceAll("//", "/");
3✔
197

198
    return baseUrl + url;
6✔
199
  }
200

201
  String get apiUrl => _makeUrl("/api/");
6✔
202

203
  String get imageUrl => _makeUrl("/image/");
×
204

205
  String makeApiUrl(String endpoint) {
3✔
206
    if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
6✔
207
      return _makeUrl(endpoint);
×
208
    } else {
209
      return _makeUrl("/api/${endpoint}");
6✔
210
    }
211
  }
212

213
  String makeUrl(String endpoint) => _makeUrl(endpoint);
×
214

215
  UserProfile? profile;
216

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

220
  // Authentication token (initially empty, must be requested)
221
  String _token = "";
222

223
  String? get serverAddress {
×
224
    return profile?.server;
×
225
  }
226

227
  bool get hasToken => _token.isNotEmpty;
3✔
228

229
  /*
230
   * Check server connection and display messages if not connected.
231
   * Useful as a precursor check before performing operations.
232
   */
233
  bool checkConnection() {
1✔
234
    // Firstly, is the server connected?
235
    if (!isConnected()) {
1✔
236

237
      showSnackIcon(
1✔
238
        L10().notConnected,
2✔
239
        success: false,
240
        icon: FontAwesomeIcons.server
241
      );
242

243
      return false;
244
    }
245

246
    // Finally
247
    return true;
248
  }
249

250
  // Server instance information
251
  String instance = "";
252

253
  // Server version information
254
  String _version = "";
255

256
  // API version of the connected server
257
  int _apiVersion = 1;
258

259
  int get apiVersion => _apiVersion;
6✔
260

261
  // API endpoint for receiving purchase order line items was introduced in v12
262
  bool get supportsPoReceive => apiVersion >= 12;
3✔
263

264
  // Notification support requires API v25 or newer
265
  bool get supportsNotifications => isConnected() && apiVersion >= 25;
4✔
266

267
  // Return True if the API supports 'settings' (requires API v46)
268
  bool get supportsSettings => isConnected() && apiVersion >= 46;
4✔
269

270
  // Part parameter support requires API v56 or newer
271
  bool get supportsPartParameters => isConnected() && apiVersion >= 56;
×
272

273
  // Supports 'modern' barcode API (v80 or newer)
274
  bool get supportModernBarcodes => isConnected() && apiVersion >= 80;
×
275

276
  // Structural categories requires API v83 or newer
277
  bool get supportsStructuralCategories => isConnected() && apiVersion >= 83;
×
278

279
  // Company attachments require API v95 or newer
280
  bool get supportCompanyAttachments => isConnected() && apiVersion >= 95;
×
281

282
  // Consolidated search request API v102 or newer
283
  bool get supportsConsolidatedSearch => isConnected() && apiVersion >= 102;
×
284

285
  // ReturnOrder supports API v104 or newer
286
  bool get supportsReturnOrders => isConnected() && apiVersion >= 104;
×
287

288
  // Status label endpoints API v105 or newer
289
  bool get supportsStatusLabelEndpoints => isConnected() && apiVersion >= 105;
×
290

291
  // Regex search API v106 or newer
292
  bool get supportsRegexSearch => isConnected() && apiVersion >= 106;
×
293

294
  // Order barcodes API v107 or newer
295
  bool get supportsOrderBarcodes => isConnected() && apiVersion >= 107;
×
296

297
  // Are plugins enabled on the server?
298
  bool _pluginsEnabled = false;
299

300
  // True plugin support requires API v34 or newer
301
  // Returns True only if the server API version is new enough, and plugins are enabled
302
  bool pluginsEnabled() => apiVersion >= 34 && _pluginsEnabled;
12✔
303

304
  // Cached list of plugins (refreshed when we connect to the server)
305
  List<InvenTreePlugin> _plugins = [];
306

307
  // Return a list of plugins enabled on the server
308
  // Can optionally filter by a particular 'mixin' type
309
  List<InvenTreePlugin> getPlugins({String mixin = ""}) {
×
310
    List<InvenTreePlugin> plugins = [];
×
311

312
    for (var plugin in _plugins) {
×
313
      // Do we wish to filter by a particular mixin?
314
      if (mixin.isNotEmpty) {
×
315
        if (!plugin.supportsMixin(mixin)) {
×
316
          continue;
317
        }
318
      }
319

320
      plugins.add(plugin);
×
321
    }
322

323
    // Return list of matching plugins
324
    return plugins;
325
  }
326

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

330
  // Getter for server version information
331
  String get version => _version;
×
332

333
  // Connection status flag - set once connection has been validated
334
  bool _connected = false;
335

336
  bool _connecting = false;
337

338
  bool isConnected() {
1✔
339
    return profile != null && _connected && baseUrl.isNotEmpty && hasToken;
5✔
340
  }
341

342
  bool isConnecting() {
1✔
343
    return !isConnected() && _connecting;
2✔
344
  }
345

346
  // Ensure we only ever create a single instance of the API class
347
  static final InvenTreeAPI _api = InvenTreeAPI._internal();
9✔
348

349
  /*
350
   * Connect to the remote InvenTree server:
351
   *
352
   * - Check that the InvenTree server exists
353
   * - Request user token from the server
354
   * - Request user roles from the server
355
   */
356
  Future<bool> _connect() async {
3✔
357

358
    if (profile == null) return false;
3✔
359

360
    String address = profile?.server ?? "";
6✔
361
    String username = profile?.username ?? "";
6✔
362
    String password = profile?.password ?? "";
6✔
363

364
    address = address.trim();
3✔
365
    username = username.trim();
3✔
366
    password = password.trim();
3✔
367

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

371
    if (address.isEmpty || username.isEmpty || password.isEmpty) {
9✔
372
      showSnackIcon(
×
373
        L10().incompleteDetails,
×
374
        icon: FontAwesomeIcons.circleExclamation,
375
        success: false
376
      );
377
      return false;
378
    }
379

380
    if (!address.endsWith("/")) {
3✔
381
      address = address + "/";
3✔
382
    }
383

384
    _BASE_URL = address;
3✔
385

386
    // Clear the list of available plugins
387
    _plugins.clear();
6✔
388

389
    debug("Connecting to ${apiUrl} -> username=${username}");
9✔
390

391
    APIResponse response;
392

393
    response = await get("", expectedStatusCode: 200);
3✔
394

395
    if (!response.successful()) {
3✔
396
      showStatusCodeError(apiUrl, response.statusCode, details: response.data.toString());
5✔
397
      return false;
398
    }
399

400
    var data = response.asMap();
3✔
401

402
    // We expect certain response from the server
403
    if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) {
9✔
404

405
      showServerError(
×
406
        apiUrl,
×
407
        L10().missingData,
×
408
        L10().serverMissingData,
×
409
      );
410

411
      return false;
412
    }
413

414
    // Record server information
415
    _version = (data["version"] ?? "") as String;
6✔
416
    instance = (data["instance"] ?? "") as String;
6✔
417

418
    // Default API version is 1 if not provided
419
    _apiVersion = (data["apiVersion"] ?? 1) as int;
6✔
420
    _pluginsEnabled = (data["plugins_enabled"] ?? false) as bool;
6✔
421

422
    if (_apiVersion < _minApiVersion) {
6✔
423

424
      String message = L10().serverApiVersion + ": ${_apiVersion}";
×
425

426
      message += "\n";
×
427
      message += L10().serverApiRequired + ": ${_minApiVersion}";
×
428

429
      message += "\n\n";
×
430

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

433
      showServerError(
×
434
        apiUrl,
×
435
        L10().serverOld,
×
436
        message,
437
      );
438

439
      return false;
440
    }
441

442
    /**
443
     * Request user token information from the server
444
     * This is the stage that we check username:password credentials!
445
     */
446
    // Clear the existing token value
447
    _token = "";
3✔
448

449
    response = await get(_URL_GET_TOKEN);
3✔
450

451
    // Invalid response
452
    if (!response.successful()) {
3✔
453

454
      switch (response.statusCode) {
1✔
455
        case 401:
1✔
456
        case 403:
×
457
          showServerError(
1✔
458
            apiUrl,
1✔
459
            L10().serverAuthenticationError,
2✔
460
            L10().invalidUsernamePassword,
2✔
461
          );
462
          break;
463
        default:
464
          showStatusCodeError(apiUrl, response.statusCode);
×
465
          break;
466
      }
467

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

470
      return false;
471
    }
472

473
    data = response.asMap();
3✔
474

475
    if (!data.containsKey("token")) {
3✔
476
      showServerError(
×
477
          apiUrl,
×
478
          L10().tokenMissing,
×
479
          L10().tokenMissingFromResponse,
×
480
      );
481

482
      return false;
483
    }
484

485
    // Return the received token
486
    _token = (data["token"] ?? "") as String;
6✔
487

488
    debug("Received token from server");
3✔
489

490
    // Request user role information (async)
491
    getUserRoles();
3✔
492

493
    // Request plugin information (async)
494
    getPluginInformation();
3✔
495

496
    // Ok, probably pretty good...
497
    return true;
498

499
  }
500

501
  void disconnectFromServer() {
3✔
502
    debug("API : disconnectFromServer()");
3✔
503

504
    _connected = false;
3✔
505
    _connecting = false;
3✔
506
    _token = "";
3✔
507
    profile = null;
3✔
508

509
    // Clear received settings
510
    _globalSettings.clear();
6✔
511
    _userSettings.clear();
6✔
512

513
    _connectionStatusChanged();
3✔
514
  }
515

516
  /*
517
   * Public facing connection function
518
   */
519
  Future<bool> connectToServer() async {
3✔
520

521
    // Ensure server is first disconnected
522
    disconnectFromServer();
3✔
523

524
    // Load selected profile
525
    profile = await UserProfileDBManager().getSelectedProfile();
9✔
526

527
    if (profile == null) {
3✔
528
      showSnackIcon(
×
529
          L10().profileSelect,
×
530
          success: false,
531
          icon: FontAwesomeIcons.circleExclamation
532
      );
533
      return false;
534
    }
535

536
    _connecting = true;
3✔
537

538
    _connectionStatusChanged();
3✔
539

540
    _connected = await _connect();
6✔
541

542
    _connecting = false;
3✔
543

544
    if (_connected) {
3✔
545
      showSnackIcon(
3✔
546
        L10().serverConnected,
6✔
547
        icon: FontAwesomeIcons.server,
548
        success: true,
549
      );
550
    }
551

552
    _connectionStatusChanged();
3✔
553

554
    return _connected;
3✔
555
  }
556

557
  /*
558
   * Request the user roles (permissions) from the InvenTree server
559
   */
560
  Future<bool> getUserRoles() async {
3✔
561

562
    roles.clear();
6✔
563

564
    debug("API: Requesting user role data");
3✔
565

566
    // Next we request the permissions assigned to the current user
567
    // Note: 2021-02-27 this "roles" feature for the API was just introduced.
568
    // Any "older" version of the server allows any API method for any logged in user!
569
    // We will return immediately, but request the user roles in the background
570

571
    final response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
3✔
572

573
    if (!response.successful()) {
3✔
574
      return false;
575
    }
576

577
    var data = response.asMap();
3✔
578

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

583
      return true;
584
    } else {
585
      return false;
586
    }
587
  }
588

589
  // Request plugin information from the server
590
  Future<void> getPluginInformation() async {
3✔
591

592
    // The server does not support plugins, or they are not enabled
593
    if (!pluginsEnabled()) {
3✔
594
      _plugins.clear();
6✔
595
      return;
596
    }
597

598
    debug("API: getPluginInformation()");
×
599

600
    // Request a list of plugins from the server
601
    final List<InvenTreeModel> results = await InvenTreePlugin().list();
×
602

603
    for (var result in results) {
×
604
      if (result is InvenTreePlugin) {
×
605
        if (result.active) {
×
606
          // Only add plugins that are active
607
          _plugins.add(result);
×
608
        }
609
      }
610
    }
611
  }
612

613
  bool checkPermission(String role, String permission) {
1✔
614
    /*
615
     * Check if the user has the given role.permission assigned
616
     *e
617
     * e.g. "part", "change"
618
     */
619

620
    // If we do not have enough information, assume permission is allowed
621
    if (roles.isEmpty) {
2✔
622
      return true;
623
    }
624

625
    if (!roles.containsKey(role)) {
2✔
626
      return true;
627
    }
628

629
    if (roles[role] == null) {
2✔
630
      return true;
631
    }
632

633
    try {
634
      List<String> perms = List.from(roles[role] as List<dynamic>);
3✔
635
      return perms.contains(permission);
1✔
636
    } catch (error, stackTrace) {
637
      if (error is TypeError) {
×
638
        // Ignore TypeError
639
      } else {
640
        // Unknown error - report it!
641
        sentryReportError(
×
642
          "api.checkPermission",
643
          error, stackTrace,
644
          context: {
×
645
            "role": role,
646
            "permission": permission,
647
            "error": error.toString(),
×
648
         }
649
        );
650
      }
651

652
      // Unable to determine permission - assume true?
653
      return true;
654
    }
655
  }
656

657

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

661
    Map<String, dynamic> _body = body;
662

663
    HttpClientRequest? request = await apiRequest(url, "PATCH");
2✔
664

665
    if (request == null) {
666
      // Return an "invalid" APIResponse
667
      return APIResponse(
×
668
        url: url,
669
        method: "PATCH",
670
        error: "HttpClientRequest is null"
671
      );
672
    }
673

674
    return completeRequest(
2✔
675
      request,
676
      data: json.encode(_body),
2✔
677
      statusCode: expectedStatusCode
678
    );
679
  }
680

681
  /*
682
   * Download a file from the given URL
683
   */
684
  Future<void> downloadFile(String url, {bool openOnDownload = true}) async {
×
685

686
    // Find the local downlods directory
687
    final Directory dir = await getTemporaryDirectory();
×
688

689
    String filename = url.split("/").last;
×
690

691
    String local_path = dir.path + "/" + filename;
×
692

693
    Uri? _uri = Uri.tryParse(makeUrl(url));
×
694

695
    if (_uri == null) {
696
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
697
      return;
698
    }
699

700
    if (_uri.host.isEmpty) {
×
701
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
702
      return;
703
    }
704

705
    HttpClientRequest? _request;
706

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

709
    var client = createClient(url, strictHttps: strictHttps);
×
710

711
    // Attempt to open a connection to the server
712
    try {
713
      _request = await client.openUrl("GET", _uri).timeout(Duration(seconds: 10));
×
714

715
      // Set headers
716
      _request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
×
717
      _request.headers.set(HttpHeaders.acceptHeader, "application/json");
×
718
      _request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
×
719
      _request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
×
720

721
    } on SocketException catch (error) {
×
722
      debug("SocketException at ${url}: ${error.toString()}");
×
723
      showServerError(url, L10().connectionRefused, error.toString());
×
724
      return;
725
    } on TimeoutException {
×
726
      debug("TimeoutException at ${url}");
×
727
      showTimeoutError(url);
×
728
      return;
729
    } on HandshakeException catch (error) {
×
730
      debug("HandshakeException at ${url}:");
×
731
      debug(error.toString());
×
732
      showServerError(url, L10().serverCertificateError, error.toString());
×
733
      return;
734
    } catch (error, stackTrace) {
735
      debug("Server error at ${url}: ${error.toString()}");
×
736
      showServerError(url, L10().serverError, error.toString());
×
737
      sentryReportError(
×
738
        "api.downloadFile : client.openUrl",
739
        error, stackTrace,
740
      );
741
      return;
742
    }
743

744
    try {
745
      final response = await _request.close();
×
746

747
      if (response.statusCode == 200) {
×
748
        var bytes = await consolidateHttpClientResponseBytes(response);
×
749

750
        File localFile = File(local_path);
×
751

752
        await localFile.writeAsBytes(bytes);
×
753

754
        if (openOnDownload) {
755
          OpenFilex.open(local_path);
×
756
        }
757
      } else {
758
        showStatusCodeError(url, response.statusCode);
×
759
      }
760
    } on SocketException catch (error) {
×
761
      showServerError(url, L10().connectionRefused, error.toString());
×
762
    } on TimeoutException {
×
763
      showTimeoutError(url);
×
764
    } catch (error, stackTrace) {
765
      debug("Error downloading image:");
×
766
      debug(error.toString());
×
767
      showServerError(url, L10().downloadError, error.toString());
×
768
      sentryReportError(
×
769
        "api.downloadFile : client.closeRequest",
770
        error, stackTrace,
771
      );
772
    }
773
  }
774

775
  /*
776
   * Upload a file to the given URL
777
   */
778
  Future<APIResponse> uploadFile(String url, File f,
×
779
      {String name = "attachment", String method="POST", Map<String, dynamic>? fields}) async {
780
    var _url = makeApiUrl(url);
×
781

782
    var request = http.MultipartRequest(method, Uri.parse(_url));
×
783

784
    request.headers.addAll(defaultHeaders());
×
785

786
    if (fields != null) {
787
      fields.forEach((String key, dynamic value) {
×
788

789
        if (value == null) {
790
          request.fields[key] = "";
×
791
        } else {
792
          request.fields[key] = value.toString();
×
793
        }
794
      });
795
    }
796

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

799
    request.files.add(_file);
×
800

801
    APIResponse response = APIResponse(
×
802
      url: url,
803
      method: method,
804
    );
805

806
    String jsondata = "";
807

808
    try {
809
      var httpResponse = await request.send().timeout(Duration(seconds: 120));
×
810

811
      response.statusCode = httpResponse.statusCode;
×
812

813
      jsondata = await httpResponse.stream.bytesToString();
×
814

815
      response.data = json.decode(jsondata);
×
816

817
      // Report a server-side error
818
      if (response.statusCode >= 500) {
×
819
        sentryReportMessage(
×
820
            "Server error in uploadFile()",
821
            context: {
×
822
              "url": url,
823
              "method": request.method,
×
824
              "name": name,
825
              "statusCode": response.statusCode.toString(),
×
826
              "requestHeaders": request.headers.toString(),
×
827
              "responseHeaders": httpResponse.headers.toString(),
×
828
            }
829
        );
830
      }
831
    } on SocketException catch (error) {
×
832
      showServerError(url, L10().connectionRefused, error.toString());
×
833
      response.error = "SocketException";
×
834
      response.errorDetail = error.toString();
×
835
    } on FormatException {
×
836
      showServerError(
×
837
        url,
838
        L10().formatException,
×
839
        L10().formatExceptionJson + ":\n${jsondata}"
×
840
      );
×
841

842
      sentryReportMessage(
×
843
          "Error decoding JSON response from server",
844
          context: {
×
845
            "method": "uploadFile",
846
            "url": url,
847
            "statusCode": response.statusCode.toString(),
×
848
            "data": jsondata,
849
          }
850
      );
851

852
    } on TimeoutException {
×
853
      showTimeoutError(url);
×
854
      response.error = "TimeoutException";
×
855
    } catch (error, stackTrace) {
856
      showServerError(url, L10().serverError, error.toString());
×
857
      sentryReportError(
×
858
        "api.uploadFile",
859
        error, stackTrace
860
      );
861
      response.error = "UnknownError";
×
862
      response.errorDetail = error.toString();
×
863
    }
864

865
    return response;
866
  }
867

868
  /*
869
   * Perform a HTTP OPTIONS request,
870
   * to get the available fields at a given endpoint.
871
   * We send this with the currently selected "locale",
872
   * so that (hopefully) the field messages are correctly translated
873
   */
874
  Future<APIResponse> options(String url) async {
×
875

876
    HttpClientRequest? request = await apiRequest(url, "OPTIONS");
×
877

878
    if (request == null) {
879
      // Return an "invalid" APIResponse
880
      return APIResponse(
×
881
        url: url,
882
        method: "OPTIONS"
883
      );
884
    }
885

886
    return completeRequest(request);
×
887
  }
888

889
  /*
890
   * Perform a HTTP POST request
891
   * Returns a json object (or null if unsuccessful)
892
   */
893
  Future<APIResponse> post(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode=201}) async {
1✔
894

895
    HttpClientRequest? request = await apiRequest(url, "POST");
1✔
896

897
    if (request == null) {
898
      // Return an "invalid" APIResponse
899
      return APIResponse(
×
900
        url: url,
901
        method: "POST"
902
      );
903
    }
904

905
    return completeRequest(
1✔
906
      request,
907
      data: json.encode(body),
1✔
908
      statusCode: expectedStatusCode
909
    );
910
  }
911

912
  /*
913
   * Perform a request to link a custom barcode to a particular item
914
   */
915
  Future<bool> linkBarcode(Map<String, String> body) async {
1✔
916

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

919
  if (request == null) {
920
    return false;
921
  }
922

923
  final response = await completeRequest(
1✔
924
    request,
925
    data: json.encode(body),
1✔
926
    statusCode: 200
927
  );
928

929
  return response.isValid() && response.statusCode == 200;
3✔
930

931
  }
932

933
  /*
934
   * Perform a request to unlink a custom barcode from a particular item
935
   */
936
  Future<bool> unlinkBarcode(Map<String, dynamic> body) async {
1✔
937

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

940
    if (request == null) {
941
      return false;
942
    }
943

944
    final response = await completeRequest(
1✔
945
        request,
946
        data: json.encode(body),
1✔
947
        statusCode: 200,
948
    );
949

950
    return response.isValid() && response.statusCode == 200;
3✔
951
  }
952

953

954
  HttpClient createClient(String url, {bool strictHttps = false}) {
3✔
955

956
    var client = HttpClient();
3✔
957

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

960
      if (strictHttps) {
961
        showServerError(
×
962
          url,
963
          L10().serverCertificateError,
×
964
          L10().serverCertificateInvalid,
×
965
        );
966
        return false;
967
      }
968

969
      // Strict HTTPs not enforced, so we'll ignore the bad cert
970
      return true;
971
    };
972

973
    // Set the connection timeout
974
    client.connectionTimeout = Duration(seconds: 30);
6✔
975

976
    return client;
977
  }
978

979
  /*
980
   * Initiate a HTTP request to the server
981
   *
982
   * @param url is the API endpoint
983
   * @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
984
   * @param params is the request parameters
985
   */
986
  Future<HttpClientRequest?> apiRequest(String url, String method, {Map<String, String> urlParams = const {}}) async {
3✔
987

988
    var _url = makeApiUrl(url);
3✔
989

990
    // Add any required query parameters to the URL using ?key=value notation
991
    if (urlParams.isNotEmpty) {
3✔
992
      String query = "?";
993

994
      urlParams.forEach((k, v) => query += "${k}=${v}&");
8✔
995

996
      _url += query;
2✔
997
    }
998

999
    // Remove extraneous character if present
1000
    if (_url.endsWith("&")) {
3✔
1001
      _url = _url.substring(0, _url.length - 1);
6✔
1002
    }
1003

1004
    Uri? _uri = Uri.tryParse(_url);
3✔
1005

1006
    if (_uri == null) {
1007
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
1008
      return null;
1009
    }
1010

1011
    if (_uri.host.isEmpty) {
6✔
1012
      showServerError(url, L10().invalidHost, L10().invalidHostDetails);
×
1013
      return null;
1014
    }
1015

1016
    HttpClientRequest? _request;
1017

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

1020
    var client = createClient(url, strictHttps: strictHttps);
3✔
1021

1022
    // Attempt to open a connection to the server
1023
    try {
1024
      _request = await client.openUrl(method, _uri).timeout(Duration(seconds: 10));
9✔
1025

1026
      // Set headers
1027
      _request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
9✔
1028
      _request.headers.set(HttpHeaders.acceptHeader, "application/json");
6✔
1029
      _request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
6✔
1030
      _request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
9✔
1031

1032
      return _request;
1033
    } on SocketException catch (error) {
1✔
1034
      debug("SocketException at ${url}: ${error.toString()}");
3✔
1035
      showServerError(url, L10().connectionRefused, error.toString());
4✔
1036
      return null;
1037
    } on TimeoutException {
×
1038
      debug("TimeoutException at ${url}");
×
1039
      showTimeoutError(url);
×
1040
      return null;
1041
    } on CertificateException catch (error) {
×
1042
      debug("CertificateException at ${url}:");
×
1043
      debug(error.toString());
×
1044
      showServerError(url, L10().serverCertificateError, error.toString());
×
1045
      return null;
1046
    } on HandshakeException catch (error) {
×
1047
      debug("HandshakeException at ${url}:");
×
1048
      debug(error.toString());
×
1049
      showServerError(url, L10().serverCertificateError, error.toString());
×
1050
      return null;
1051
    } catch (error, stackTrace) {
1052
      debug("Server error at ${url}: ${error.toString()}");
×
1053
      showServerError(url, L10().serverError, error.toString());
×
1054
      sentryReportError(
×
1055
        "api.apiRequest : openUrl",
1056
        error, stackTrace,
1057
        context: {
×
1058
          "url": url,
1059
          "method": method,
1060
        }
1061
      );
1062

1063
      return null;
1064
    }
1065
  }
1066

1067

1068
  /*
1069
   * Complete an API request, and return an APIResponse object
1070
   */
1071
  Future<APIResponse> completeRequest(HttpClientRequest request, {String? data, int? statusCode, bool ignoreResponse = false}) async {
3✔
1072

1073
    if (data != null && data.isNotEmpty) {
2✔
1074

1075
      var encoded_data = utf8.encode(data);
2✔
1076

1077
      request.headers.set(HttpHeaders.contentLengthHeader, encoded_data.length.toString());
8✔
1078
      request.add(encoded_data);
2✔
1079
    }
1080

1081
    APIResponse response = APIResponse(
3✔
1082
      method: request.method,
3✔
1083
      url: request.uri.toString()
6✔
1084
    );
1085

1086
    String url = request.uri.toString();
6✔
1087

1088
    try {
1089
      HttpClientResponse? _response = await request.close().timeout(Duration(seconds: 10));
9✔
1090

1091
      response.statusCode = _response.statusCode;
6✔
1092

1093
      // If the server returns a server error code, alert the user
1094
      if (_response.statusCode >= 500) {
6✔
1095
        showStatusCodeError(url, _response.statusCode);
×
1096

1097
        // Some server errors are not ones for us to worry about!
1098
        switch (_response.statusCode) {
×
1099
          case 502:   // Bad gateway
×
1100
          case 504:   // Gateway timeout
×
1101
            break;
1102
          default:    // Any other error code
1103
            sentryReportMessage(
×
1104
                "Server error",
1105
                context: {
×
1106
                  "url": request.uri.toString(),
×
1107
                  "method": request.method,
×
1108
                  "statusCode": _response.statusCode.toString(),
×
1109
                  "requestHeaders": request.headers.toString(),
×
1110
                  "responseHeaders": _response.headers.toString(),
×
1111
                  "responseData": response.data.toString(),
×
1112
                }
1113
            );
1114
            break;
1115
        }
1116
      } else {
1117

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

1120
        // First check that the returned status code is what we expected
1121
        if (statusCode != null && statusCode != _response.statusCode) {
4✔
1122
          showStatusCodeError(url, _response.statusCode, details: response.data.toString());
×
1123
        }
1124
      }
1125
    } on HttpException catch (error) {
×
1126
      showServerError(url, L10().serverError, error.toString());
×
1127
      response.error = "HTTPException";
×
1128
      response.errorDetail = error.toString();
×
1129
    } on SocketException catch (error) {
×
1130
      showServerError(url, L10().connectionRefused, error.toString());
×
1131
      response.error = "SocketException";
×
1132
      response.errorDetail = error.toString();
×
1133
    } on CertificateException catch (error) {
×
1134
      debug("CertificateException at ${request.uri.toString()}:");
×
1135
      debug(error.toString());
×
1136
      showServerError(url, L10().serverCertificateError, error.toString());
×
1137
    } on TimeoutException {
×
1138
      showTimeoutError(url);
×
1139
      response.error = "TimeoutException";
×
1140
    } catch (error, stackTrace) {
1141
      showServerError(url, L10().serverError, error.toString());
×
1142
      sentryReportError("api.completeRequest", error, stackTrace);
×
1143
      response.error = "UnknownError";
×
1144
      response.errorDetail = error.toString();
×
1145
    }
1146

1147
    return response;
1148

1149
  }
1150

1151
  /*
1152
   * Convert a HttpClientResponse response object to JSON
1153
   */
1154
  dynamic responseToJson(String url, HttpClientResponse response) async {
3✔
1155

1156
    String body = await response.transform(utf8.decoder).join();
9✔
1157

1158
    try {
1159
      var data = json.decode(body);
3✔
1160

1161
      return data ?? {};
×
1162
    } on FormatException {
×
1163

1164
      switch (response.statusCode) {
×
1165
        case 400:
×
1166
        case 401:
×
1167
        case 403:
×
1168
        case 404:
×
1169
          // Ignore for unauthorized pages
1170
          break;
1171
        default:
1172
          sentryReportMessage(
×
1173
              "Error decoding JSON response from server",
1174
              context: {
×
1175
                "headers": response.headers.toString(),
×
1176
                "statusCode": response.statusCode.toString(),
×
1177
                "data": body.toString(),
×
1178
                "endpoint": url,
1179
              }
1180
          );
1181
          break;
1182
      }
1183

1184
      showServerError(
×
1185
        url,
1186
        L10().formatException,
×
1187
        L10().formatExceptionJson + ":\n${body}"
×
1188
      );
×
1189

1190
      // Return an empty map
1191
      return {};
×
1192
    }
1193

1194
  }
1195

1196
  /*
1197
   * Perform a HTTP GET request
1198
   * Returns a json object (or null if did not complete)
1199
   */
1200
  Future<APIResponse> get(String url, {Map<String, String> params = const {}, int? expectedStatusCode=200}) async {
3✔
1201

1202
    HttpClientRequest? request = await apiRequest(
3✔
1203
      url,
1204
      "GET",
1205
      urlParams: params,
1206
    );
1207

1208
    if (request == null) {
1209
      // Return an "invalid" APIResponse
1210
      return APIResponse(
1✔
1211
        url: url,
1212
        method: "GET",
1213
        error: "HttpClientRequest is null",
1214
      );
1215
    }
1216

1217
    return completeRequest(request);
3✔
1218
  }
1219

1220
  /*
1221
   * Perform a HTTP DELETE request
1222
   */
1223
  Future<APIResponse> delete(String url) async {
×
1224

1225
    HttpClientRequest? request = await apiRequest(
×
1226
      url,
1227
      "DELETE",
1228
    );
1229

1230
    if (request == null) {
1231
      // Return an "invalid" APIResponse object
1232
      return APIResponse(
×
1233
        url: url,
1234
        method: "DELETE",
1235
        error: "HttpClientRequest is null",
1236
      );
1237
    }
1238

1239
    return completeRequest(
×
1240
      request,
1241
      ignoreResponse: true,
1242
    );
1243
  }
1244

1245
  // Return a list of request headers
1246
  Map<String, String> defaultHeaders() {
×
1247
    Map<String, String> headers = {};
×
1248

1249
    headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
×
1250
    headers[HttpHeaders.acceptHeader] = "application/json";
×
1251
    headers[HttpHeaders.contentTypeHeader] = "application/json";
×
1252
    headers[HttpHeaders.acceptLanguageHeader] = Intl.getCurrentLocale();
×
1253

1254
    return headers;
1255
  }
1256

1257
  String _authorizationHeader() {
3✔
1258
    if (_token.isNotEmpty) {
6✔
1259
      return "Token $_token";
6✔
1260
    } else if (profile != null) {
3✔
1261
      return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}"));
24✔
1262
    } else {
1263
      return "";
1264
    }
1265
  }
1266

1267
  static String get staticImage => "/static/img/blank_image.png";
×
1268

1269
  static String get staticThumb => "/static/img/blank_image.thumbnail.png";
×
1270

1271
  /*
1272
   * Load image from the InvenTree server,
1273
   * or from local cache (if it has been cached!)
1274
   */
1275
  CachedNetworkImage getImage(String imageUrl, {double? height, double? width}) {
×
1276
    if (imageUrl.isEmpty) {
×
1277
      imageUrl = staticImage;
×
1278
    }
1279

1280
    String url = makeUrl(imageUrl);
×
1281

1282
    const key = "inventree_network_image";
1283

1284
    CacheManager manager = CacheManager(
×
1285
      Config(
×
1286
        key,
1287
        fileService: InvenTreeFileService(
×
1288
          strictHttps: _strictHttps,
×
1289
        ),
1290
      )
1291
    );
1292

1293
    return CachedNetworkImage(
×
1294
      imageUrl: url,
1295
      placeholder: (context, url) => CircularProgressIndicator(),
×
1296
      errorWidget: (context, url, error) => FaIcon(FontAwesomeIcons.circleXmark, color: COLOR_DANGER),
×
1297
      httpHeaders: defaultHeaders(),
×
1298
      height: height,
1299
      width: width,
1300
      cacheManager: manager,
1301
    );
1302
  }
1303

1304
  // Keep a record of which settings we have received from the server
1305
  Map<String, InvenTreeGlobalSetting> _globalSettings = {};
1306
  Map<String, InvenTreeUserSetting> _userSettings = {};
1307

1308
  Future<String> getGlobalSetting(String key) async {
×
1309
    if (!supportsSettings) return "";
×
1310

1311
    InvenTreeGlobalSetting? setting = _globalSettings[key];
×
1312

1313
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1314
      return setting.value;
×
1315
    }
1316

1317
    final response = await InvenTreeGlobalSetting().getModel(key);
×
1318

1319
    if (response is InvenTreeGlobalSetting) {
×
1320
      response.lastReload = DateTime.now();
×
1321
      _globalSettings[key] = response;
×
1322
      return response.value;
×
1323
    } else {
1324
      return "";
1325
    }
1326
  }
1327

1328
  Future<String> getUserSetting(String key) async {
×
1329
    if (!supportsSettings) return "";
×
1330

1331
    InvenTreeUserSetting? setting = _userSettings[key];
×
1332

1333
    if ((setting != null) && setting.reloadedWithin(Duration(minutes: 5))) {
×
1334
      return setting.value;
×
1335
    }
1336

1337
    final response = await InvenTreeGlobalSetting().getModel(key);
×
1338

1339
    if (response is InvenTreeUserSetting) {
×
1340
      response.lastReload = DateTime.now();
×
1341
      _userSettings[key] = response;
×
1342
      return response.value;
×
1343
    } else {
1344
      return "";
1345
    }
1346
  }
1347

1348
  /*
1349
   * Send a request to the server to locate / identify either a StockItem or StockLocation
1350
   */
1351
  Future<void> locateItemOrLocation(BuildContext context, {int? item, int? location}) async {
×
1352

1353
    var plugins = getPlugins(mixin: "locate");
×
1354

1355
    if (plugins.isEmpty) {
×
1356
      // TODO: Error message
1357
      return;
1358
    }
1359

1360
    String plugin_name = "";
1361

1362
    if (plugins.length == 1) {
×
1363
      plugin_name = plugins.first.key;
×
1364
    } else {
1365
      // User selects which plugin to use
1366
      List<Map<String, dynamic>> plugin_options = [];
×
1367

1368
      for (var plugin in plugins) {
×
1369
        plugin_options.add({
×
1370
          "display_name": plugin.humanName,
×
1371
          "value": plugin.key,
×
1372
        });
1373
      }
1374

1375
      Map<String, dynamic> fields = {
×
1376
        "plugin": {
×
1377
          "label": L10().plugin,
×
1378
          "type": "choice",
1379
          "value": plugins.first.key,
×
1380
          "choices": plugin_options,
1381
          "required": true,
1382
        }
1383
      };
1384

1385
      await launchApiForm(
×
1386
          context,
1387
          L10().locateLocation,
×
1388
          "",
1389
          fields,
1390
          icon: FontAwesomeIcons.magnifyingGlassLocation,
1391
          onSuccess: (Map<String, dynamic> data) async {
×
1392
            plugin_name = (data["plugin"] ?? "") as String;
×
1393
          }
1394
      );
1395
    }
1396

1397
    Map<String, dynamic> body = {
×
1398
      "plugin": plugin_name,
1399
    };
1400

1401
    if (item != null) {
1402
      body["item"] = item.toString();
×
1403
    }
1404

1405
    if (location != null) {
1406
      body["location"] = location.toString();
×
1407
    }
1408

1409
    post(
×
1410
      "/api/locate/",
1411
      body: body,
1412
      expectedStatusCode: 200,
1413
    ).then((APIResponse response) {
×
1414
      if (response.successful()) {
×
1415
        showSnackIcon(
×
1416
          L10().requestSuccessful,
×
1417
          success: true,
1418
        );
1419
      }
1420
    });
1421
  }
1422
}
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