mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-11-04 07:15:46 +00:00 
			
		
		
		
	@@ -1,6 +1,17 @@
 | 
				
			|||||||
## InvenTree App Release Notes
 | 
					## InvenTree App Release Notes
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 0.3.1 - July 2021
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Adds new "API driven" forms
 | 
				
			||||||
 | 
					- Improvements for Part editing form
 | 
				
			||||||
 | 
					- Improvements for PartCategory editing form
 | 
				
			||||||
 | 
					- Improvements for StockLocation editing form
 | 
				
			||||||
 | 
					- Adds ability to edit StockItem
 | 
				
			||||||
 | 
					- Display purchase price (where available) for StockItem
 | 
				
			||||||
 | 
					- Updated translations 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 0.2.10 - July 2021
 | 
					### 0.2.10 - July 2021
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,8 @@ export "COCOAPODS_PARALLEL_CODE_SIGN=true"
 | 
				
			|||||||
export "FLUTTER_TARGET=lib\main.dart"
 | 
					export "FLUTTER_TARGET=lib\main.dart"
 | 
				
			||||||
export "FLUTTER_BUILD_DIR=build"
 | 
					export "FLUTTER_BUILD_DIR=build"
 | 
				
			||||||
export "SYMROOT=${SOURCE_ROOT}/../build\ios"
 | 
					export "SYMROOT=${SOURCE_ROOT}/../build\ios"
 | 
				
			||||||
export "FLUTTER_BUILD_NAME=0.2.10"
 | 
					export "FLUTTER_BUILD_NAME=0.3.1"
 | 
				
			||||||
export "FLUTTER_BUILD_NUMBER=18"
 | 
					export "FLUTTER_BUILD_NUMBER=19"
 | 
				
			||||||
export "DART_OBFUSCATION=false"
 | 
					export "DART_OBFUSCATION=false"
 | 
				
			||||||
export "TRACK_WIDGET_CREATION=false"
 | 
					export "TRACK_WIDGET_CREATION=false"
 | 
				
			||||||
export "TREE_SHAKE_ICONS=false"
 | 
					export "TREE_SHAKE_ICONS=false"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										29
									
								
								lib/api.dart
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								lib/api.dart
									
									
									
									
									
								
							@@ -2,20 +2,20 @@ import 'dart:async';
 | 
				
			|||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:http/http.dart' as http;
 | 
				
			||||||
import 'package:intl/intl.dart';
 | 
					import 'package:intl/intl.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:inventree/inventree/sentry.dart';
 | 
					 | 
				
			||||||
import 'package:inventree/user_profile.dart';
 | 
					 | 
				
			||||||
import 'package:inventree/widget/snacks.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/cupertino.dart';
 | 
					import 'package:flutter/cupertino.dart';
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
					import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
				
			||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:inventree/widget/dialogs.dart';
 | 
					import 'package:inventree/widget/dialogs.dart';
 | 
				
			||||||
import 'package:inventree/l10.dart';
 | 
					import 'package:inventree/l10.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/inventree/sentry.dart';
 | 
				
			||||||
import 'package:http/http.dart' as http;
 | 
					import 'package:inventree/user_profile.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/widget/snacks.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -93,7 +93,7 @@ class InvenTreeFileService extends FileService {
 | 
				
			|||||||
class InvenTreeAPI {
 | 
					class InvenTreeAPI {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Minimum required API version for server
 | 
					  // Minimum required API version for server
 | 
				
			||||||
  static const _minApiVersion = 6;
 | 
					  static const _minApiVersion = 7;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Endpoint for requesting an API token
 | 
					  // Endpoint for requesting an API token
 | 
				
			||||||
  static const _URL_GET_TOKEN = "user/token/";
 | 
					  static const _URL_GET_TOKEN = "user/token/";
 | 
				
			||||||
@@ -128,7 +128,13 @@ class InvenTreeAPI {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String get imageUrl => _makeUrl("/image/");
 | 
					  String get imageUrl => _makeUrl("/image/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String makeApiUrl(String endpoint) => _makeUrl("/api/" + endpoint);
 | 
					  String makeApiUrl(String endpoint) {
 | 
				
			||||||
 | 
					    if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
 | 
				
			||||||
 | 
					      return _makeUrl(endpoint);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return _makeUrl("/api/" + endpoint);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String makeUrl(String endpoint) => _makeUrl(endpoint);
 | 
					  String makeUrl(String endpoint) => _makeUrl(endpoint);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -431,7 +437,7 @@ class InvenTreeAPI {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Perform a PATCH request
 | 
					  // Perform a PATCH request
 | 
				
			||||||
  Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int expectedStatusCode=200}) async {
 | 
					  Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int? expectedStatusCode}) async {
 | 
				
			||||||
    var _body = Map<String, String>();
 | 
					    var _body = Map<String, String>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Copy across provided data
 | 
					    // Copy across provided data
 | 
				
			||||||
@@ -593,8 +599,6 @@ class InvenTreeAPI {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Uri? _uri = Uri.tryParse(_url);
 | 
					    Uri? _uri = Uri.tryParse(_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    print("apiRequest ${method} -> ${url}");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (_uri == null) {
 | 
					    if (_uri == null) {
 | 
				
			||||||
      showServerError(L10().invalidHost, L10().invalidHostDetails);
 | 
					      showServerError(L10().invalidHost, L10().invalidHostDetails);
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
@@ -621,12 +625,15 @@ class InvenTreeAPI {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      return _request;
 | 
					      return _request;
 | 
				
			||||||
    } on SocketException catch (error) {
 | 
					    } on SocketException catch (error) {
 | 
				
			||||||
 | 
					      print("SocketException at ${url}: ${error.toString()}");
 | 
				
			||||||
      showServerError(L10().connectionRefused, error.toString());
 | 
					      showServerError(L10().connectionRefused, error.toString());
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    } on TimeoutException {
 | 
					    } on TimeoutException {
 | 
				
			||||||
 | 
					      print("TimeoutException at ${url}");
 | 
				
			||||||
      showTimeoutError();
 | 
					      showTimeoutError();
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    } catch (error, stackTrace) {
 | 
					    } catch (error, stackTrace) {
 | 
				
			||||||
 | 
					      print("Server error at ${url}: ${error.toString()}");
 | 
				
			||||||
      showServerError(L10().serverError, error.toString());
 | 
					      showServerError(L10().serverError, error.toString());
 | 
				
			||||||
      sentryReportError(error, stackTrace);
 | 
					      sentryReportError(error, stackTrace);
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
@@ -809,4 +816,4 @@ class InvenTreeAPI {
 | 
				
			|||||||
      cacheManager: manager,
 | 
					      cacheManager: manager,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										681
									
								
								lib/api_form.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										681
									
								
								lib/api_form.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,681 @@
 | 
				
			|||||||
 | 
					import 'dart:ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
				
			||||||
 | 
					import 'package:dropdown_search/dropdown_search.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:inventree/api.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/inventree/part.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/inventree/stock.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/widget/fields.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/l10.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/cupertino.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/widget/snacks.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Class that represents a single "form field",
 | 
				
			||||||
 | 
					 * defined by the InvenTree API
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class APIFormField {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _controller = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Constructor
 | 
				
			||||||
 | 
					  APIFormField(this.name, this.data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Name of this field
 | 
				
			||||||
 | 
					  final String name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // JSON data which defines the field
 | 
				
			||||||
 | 
					  final dynamic data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dynamic initial_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get the "api_url" associated with a related field
 | 
				
			||||||
 | 
					  String get api_url => data["api_url"] ?? "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get the "model" associated with a related field
 | 
				
			||||||
 | 
					  String get model => data["model"] ?? "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Is this field hidden?
 | 
				
			||||||
 | 
					  bool get hidden => (data['hidden'] ?? false) as bool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Is this field read only?
 | 
				
			||||||
 | 
					  bool get readOnly => (data['read_only'] ?? false) as bool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get the "value" as a string (look for "default" if not available)
 | 
				
			||||||
 | 
					  dynamic get value => (data['value'] ?? data['default']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get the "default" as a string
 | 
				
			||||||
 | 
					  dynamic get defaultValue => data['default'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, String> get filters {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Map<String, String> _filters = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Start with the provided "model" filters
 | 
				
			||||||
 | 
					    if (data.containsKey("filters")) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dynamic f = data["filters"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (f is Map) {
 | 
				
			||||||
 | 
					        f.forEach((key, value) {
 | 
				
			||||||
 | 
					          _filters[key] = value.toString();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Now, look at the provided "instance_filters"
 | 
				
			||||||
 | 
					    if (data.containsKey("instance_filters")) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dynamic f = data["instance_filters"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (f is Map) {
 | 
				
			||||||
 | 
					        f.forEach((key, value) {
 | 
				
			||||||
 | 
					          _filters[key] = value.toString();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return _filters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool hasErrors() => errorMessages().length > 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Return the error message associated with this field
 | 
				
			||||||
 | 
					  List<String> errorMessages() {
 | 
				
			||||||
 | 
					    List<dynamic> errors = data['errors'] ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    List<String> messages = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (dynamic error in errors) {
 | 
				
			||||||
 | 
					      messages.add(error.toString());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return messages;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Is this field required?
 | 
				
			||||||
 | 
					  bool get required => (data['required'] ?? false) as bool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get type => (data['type'] ?? '').toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get label => (data['label'] ?? '').toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get helpText => (data['help_text'] ?? '').toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get placeholderText => (data['placeholder'] ?? '').toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<dynamic> get choices => data["choices"] ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> loadInitialData() async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Only for "related fields"
 | 
				
			||||||
 | 
					    if (type != "related field") {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Null value? No point!
 | 
				
			||||||
 | 
					    if (value == null) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    int? pk = int.tryParse(value.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (pk == null) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    String url = api_url + "/" + pk.toString() + "/";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final APIResponse response = await InvenTreeAPI().get(
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      params: filters,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response.isValid()) {
 | 
				
			||||||
 | 
					      initial_data = response.data;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Construct a widget for this input
 | 
				
			||||||
 | 
					  Widget constructField() {
 | 
				
			||||||
 | 
					    switch (type) {
 | 
				
			||||||
 | 
					      case "string":
 | 
				
			||||||
 | 
					      case "url":
 | 
				
			||||||
 | 
					        return _constructString();
 | 
				
			||||||
 | 
					      case "boolean":
 | 
				
			||||||
 | 
					        return _constructBoolean();
 | 
				
			||||||
 | 
					      case "related field":
 | 
				
			||||||
 | 
					        return _constructRelatedField();
 | 
				
			||||||
 | 
					      case "choice":
 | 
				
			||||||
 | 
					        return _constructChoiceField();
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return ListTile(
 | 
				
			||||||
 | 
					          title: Text(
 | 
				
			||||||
 | 
					            "Unsupported field type: '${type}'",
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					                color: COLOR_DANGER,
 | 
				
			||||||
 | 
					                fontStyle: FontStyle.italic),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _constructChoiceField() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dynamic _initial;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if the current value is within the allowed values
 | 
				
			||||||
 | 
					    for (var opt in choices) {
 | 
				
			||||||
 | 
					      if (opt['value'] == value) {
 | 
				
			||||||
 | 
					        _initial = opt;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return DropdownSearch<dynamic>(
 | 
				
			||||||
 | 
					      mode: Mode.BOTTOM_SHEET,
 | 
				
			||||||
 | 
					      showSelectedItem: false,
 | 
				
			||||||
 | 
					      selectedItem: _initial,
 | 
				
			||||||
 | 
					      items: choices,
 | 
				
			||||||
 | 
					      label: label,
 | 
				
			||||||
 | 
					      hint: helpText,
 | 
				
			||||||
 | 
					      onChanged: null,
 | 
				
			||||||
 | 
					      autoFocusSearchBox: true,
 | 
				
			||||||
 | 
					      showClearButton: !required,
 | 
				
			||||||
 | 
					      itemAsString: (dynamic item) {
 | 
				
			||||||
 | 
					        return item['display_name'];
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onSaved: (item) {
 | 
				
			||||||
 | 
					        if (item == null) {
 | 
				
			||||||
 | 
					          data['value'] = null;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          data['value'] = item['value'];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Construct an input for a related field
 | 
				
			||||||
 | 
					  Widget _constructRelatedField() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return DropdownSearch<dynamic>(
 | 
				
			||||||
 | 
					      mode: Mode.BOTTOM_SHEET,
 | 
				
			||||||
 | 
					      showSelectedItem: true,
 | 
				
			||||||
 | 
					      selectedItem: initial_data,
 | 
				
			||||||
 | 
					      onFind: (String filter) async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Map<String, String> _filters = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        filters.forEach((key, value) {
 | 
				
			||||||
 | 
					          _filters[key] = value;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _filters["search"] = filter;
 | 
				
			||||||
 | 
					        _filters["offset"] = "0";
 | 
				
			||||||
 | 
					        _filters["limit"] = "25";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final APIResponse response = await InvenTreeAPI().get(
 | 
				
			||||||
 | 
					          api_url,
 | 
				
			||||||
 | 
					          params: _filters
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.isValid()) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          List<dynamic> results = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          for (var result in response.data['results'] ?? []) {
 | 
				
			||||||
 | 
					            results.add(result);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return results;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      label: label,
 | 
				
			||||||
 | 
					      hint: helpText,
 | 
				
			||||||
 | 
					      onChanged: null,
 | 
				
			||||||
 | 
					      showClearButton: !required,
 | 
				
			||||||
 | 
					      itemAsString: (dynamic item) {
 | 
				
			||||||
 | 
					        return item['pathstring'];
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      dropdownBuilder: (context, item, itemAsString) {
 | 
				
			||||||
 | 
					        return _renderRelatedField(item, true, false);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      popupItemBuilder: (context, item, isSelected) {
 | 
				
			||||||
 | 
					        return _renderRelatedField(item, isSelected, true);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onSaved: (item) {
 | 
				
			||||||
 | 
					        if (item != null) {
 | 
				
			||||||
 | 
					          data['value'] = item['pk'] ?? null;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          data['value'] = null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      isFilteredOnline: true,
 | 
				
			||||||
 | 
					      showSearchBox: true,
 | 
				
			||||||
 | 
					      autoFocusSearchBox: true,
 | 
				
			||||||
 | 
					      compareFn: (dynamic item, dynamic selectedItem) {
 | 
				
			||||||
 | 
					        // Comparison is based on the PK value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (item == null || selectedItem == null) {
 | 
				
			||||||
 | 
					          return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return item['pk'] == selectedItem['pk'];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _renderRelatedField(dynamic item, bool selected, bool extended) {
 | 
				
			||||||
 | 
					    // Render a "related field" based on the "model" type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (item == null) {
 | 
				
			||||||
 | 
					      return Text(
 | 
				
			||||||
 | 
					        helpText,
 | 
				
			||||||
 | 
					        style: TextStyle(
 | 
				
			||||||
 | 
					          fontStyle: FontStyle.italic
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (model) {
 | 
				
			||||||
 | 
					      case "partcategory":
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var cat = InvenTreePartCategory.fromJson(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ListTile(
 | 
				
			||||||
 | 
					          title: Text(
 | 
				
			||||||
 | 
					            cat.pathstring,
 | 
				
			||||||
 | 
					            style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal)
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          subtitle: extended ? Text(
 | 
				
			||||||
 | 
					            cat.description,
 | 
				
			||||||
 | 
					            style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
 | 
				
			||||||
 | 
					          ) : null,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case "stocklocation":
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var loc = InvenTreeStockLocation.fromJson(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ListTile(
 | 
				
			||||||
 | 
					          title: Text(
 | 
				
			||||||
 | 
					            loc.pathstring,
 | 
				
			||||||
 | 
					              style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal)
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          subtitle: extended ? Text(
 | 
				
			||||||
 | 
					            loc.description,
 | 
				
			||||||
 | 
					            style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
 | 
				
			||||||
 | 
					          ) : null,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return ListTile(
 | 
				
			||||||
 | 
					          title: Text(
 | 
				
			||||||
 | 
					            "Unsupported model",
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					              fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					              color: COLOR_DANGER
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          subtitle: Text("Model '${model}' rendering not supported"),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Construct a string input element
 | 
				
			||||||
 | 
					  Widget _constructString() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return TextFormField(
 | 
				
			||||||
 | 
					      decoration: InputDecoration(
 | 
				
			||||||
 | 
					        labelText: required ? label + "*" : label,
 | 
				
			||||||
 | 
					        labelStyle: _labelStyle(),
 | 
				
			||||||
 | 
					        helperText: helpText,
 | 
				
			||||||
 | 
					        helperStyle: _helperStyle(),
 | 
				
			||||||
 | 
					        hintText: placeholderText,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      initialValue: value ?? '',
 | 
				
			||||||
 | 
					      onSaved: (val) {
 | 
				
			||||||
 | 
					        data["value"] = val;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      validator: (value) {
 | 
				
			||||||
 | 
					        if (required && (value == null || value.isEmpty)) {
 | 
				
			||||||
 | 
					          // return L10().valueCannotBeEmpty;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Construct a boolean input element
 | 
				
			||||||
 | 
					  Widget _constructBoolean() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return CheckBoxField(
 | 
				
			||||||
 | 
					      label: label,
 | 
				
			||||||
 | 
					      labelStyle: _labelStyle(),
 | 
				
			||||||
 | 
					      helperText: helpText,
 | 
				
			||||||
 | 
					      helperStyle: _helperStyle(),
 | 
				
			||||||
 | 
					      initial: value,
 | 
				
			||||||
 | 
					      onSaved: (val) {
 | 
				
			||||||
 | 
					        data['value'] = val;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TextStyle _labelStyle() {
 | 
				
			||||||
 | 
					    return new TextStyle(
 | 
				
			||||||
 | 
					      fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					      fontSize: 18,
 | 
				
			||||||
 | 
					      fontFamily: "arial",
 | 
				
			||||||
 | 
					      color: hasErrors() ? COLOR_DANGER : COLOR_GRAY,
 | 
				
			||||||
 | 
					      fontStyle: FontStyle.normal,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TextStyle _helperStyle() {
 | 
				
			||||||
 | 
					    return new TextStyle(
 | 
				
			||||||
 | 
					      fontStyle: FontStyle.italic,
 | 
				
			||||||
 | 
					      color: hasErrors() ? COLOR_DANGER : COLOR_GRAY,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Extract field options from a returned OPTIONS request
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					Map<String, dynamic> extractFields(APIResponse response) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!response.isValid()) {
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!response.data.containsKey("actions")) {
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var actions = response.data["actions"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Launch an API-driven form,
 | 
				
			||||||
 | 
					 * which uses the OPTIONS metadata (at the provided URL)
 | 
				
			||||||
 | 
					 * to determine how the form elements should be rendered!
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param title is the title text to display on the form
 | 
				
			||||||
 | 
					 * @param url is the API URl to make the OPTIONS request to
 | 
				
			||||||
 | 
					 * @param fields is a map of fields to display (with optional overrides)
 | 
				
			||||||
 | 
					 * @param modelData is the (optional) existing modelData
 | 
				
			||||||
 | 
					 * @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {Map<String, dynamic> modelData = const {}, String method = "PATCH", Function? onSuccess, Function? onCancel}) async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var options = await InvenTreeAPI().options(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Invalid response from server
 | 
				
			||||||
 | 
					  if (!options.isValid()) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var availableFields = extractFields(options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (availableFields.isEmpty) {
 | 
				
			||||||
 | 
					    // User does not have permission to perform this action
 | 
				
			||||||
 | 
					    showSnackIcon(
 | 
				
			||||||
 | 
					      L10().response403,
 | 
				
			||||||
 | 
					      icon: FontAwesomeIcons.userTimes,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Construct a list of APIFormField objects
 | 
				
			||||||
 | 
					  List<APIFormField> formFields = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Iterate through the provided fields we wish to display
 | 
				
			||||||
 | 
					  for (String fieldName in fields.keys) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check that the field is actually available at the API endpoint
 | 
				
			||||||
 | 
					    if (!availableFields.containsKey(fieldName)) {
 | 
				
			||||||
 | 
					      print("Field '${fieldName}' not available at '${url}'");
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var remoteField = availableFields[fieldName] ?? {};
 | 
				
			||||||
 | 
					    var localField = fields[fieldName] ?? {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Override defined field parameters, if provided
 | 
				
			||||||
 | 
					    for (String key in localField.keys) {
 | 
				
			||||||
 | 
					      // Special consideration must be taken here!
 | 
				
			||||||
 | 
					      if (key == "filters") {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!remoteField.containsKey("filters")) {
 | 
				
			||||||
 | 
					          remoteField["filters"] = {};
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var filters = localField["filters"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (filters is Map) {
 | 
				
			||||||
 | 
					          filters.forEach((key, value) {
 | 
				
			||||||
 | 
					            remoteField["filters"][key] = value;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        remoteField[key] = localField[key];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Update fields with existing model data
 | 
				
			||||||
 | 
					    for (String key in modelData.keys) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dynamic value = modelData[key];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (availableFields.containsKey(key)) {
 | 
				
			||||||
 | 
					        availableFields[key]['value'] = value;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    formFields.add(APIFormField(fieldName, remoteField));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Grab existing data for each form field
 | 
				
			||||||
 | 
					  for (var field in formFields) {
 | 
				
			||||||
 | 
					    await field.loadInitialData();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Now, launch a new widget!
 | 
				
			||||||
 | 
					  Navigator.push(
 | 
				
			||||||
 | 
					    context,
 | 
				
			||||||
 | 
					    MaterialPageRoute(builder: (context) => APIFormWidget(
 | 
				
			||||||
 | 
					        title,
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        formFields,
 | 
				
			||||||
 | 
					        onSuccess: onSuccess,
 | 
				
			||||||
 | 
					    ))
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class APIFormWidget extends StatefulWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //! Form title to display
 | 
				
			||||||
 | 
					  final String title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //! API URL
 | 
				
			||||||
 | 
					  final String url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final List<APIFormField> fields;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Function? onSuccess;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  APIFormWidget(
 | 
				
			||||||
 | 
					      this.title,
 | 
				
			||||||
 | 
					      this.url,
 | 
				
			||||||
 | 
					      this.fields,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        Key? key,
 | 
				
			||||||
 | 
					        this.onSuccess,
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  ) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  _APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, onSuccess);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _APIFormWidgetState extends State<APIFormWidget> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _formKey = new GlobalKey<FormState>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<APIFormField> fields;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Function? onSuccess;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _APIFormWidgetState(this.title, this.url, this.fields, this.onSuccess) : super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<Widget> _buildForm() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    List<Widget> widgets = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var field in fields) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (field.hidden) {
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      widgets.add(field.constructField());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (field.hasErrors()) {
 | 
				
			||||||
 | 
					        for (String error in field.errorMessages()) {
 | 
				
			||||||
 | 
					          widgets.add(
 | 
				
			||||||
 | 
					            ListTile(
 | 
				
			||||||
 | 
					              title: Text(
 | 
				
			||||||
 | 
					                error,
 | 
				
			||||||
 | 
					                style: TextStyle(
 | 
				
			||||||
 | 
					                  color: COLOR_DANGER,
 | 
				
			||||||
 | 
					                  fontStyle: FontStyle.italic,
 | 
				
			||||||
 | 
					                  fontSize: 16,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return widgets;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _save(BuildContext context) async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Package up the form data
 | 
				
			||||||
 | 
					    Map<String, String> _data = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var field in fields) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dynamic value = field.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (value == null) {
 | 
				
			||||||
 | 
					        _data[field.name] = "";
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        _data[field.name] = value.toString();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO: Handle "POST" forms too!!
 | 
				
			||||||
 | 
					    final response = await InvenTreeAPI().patch(
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      body: _data,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!response.isValid()) {
 | 
				
			||||||
 | 
					      // TODO: Display an error message!
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (response.statusCode) {
 | 
				
			||||||
 | 
					      case 200:
 | 
				
			||||||
 | 
					      case 201:
 | 
				
			||||||
 | 
					        // Form was successfully validated by the server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Hide this form
 | 
				
			||||||
 | 
					        Navigator.pop(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // TODO: Display a snackBar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Run custom onSuccess function
 | 
				
			||||||
 | 
					        var successFunc = onSuccess;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (successFunc != null) {
 | 
				
			||||||
 | 
					          successFunc();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      case 400:
 | 
				
			||||||
 | 
					        // Form submission / validation error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update field errors
 | 
				
			||||||
 | 
					        for (var field in fields) {
 | 
				
			||||||
 | 
					          field.data['errors'] = response.data[field.name];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      // TODO: Other status codes?
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      // Refresh the form
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        title: Text(title),
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					          IconButton(
 | 
				
			||||||
 | 
					            icon: FaIcon(FontAwesomeIcons.save),
 | 
				
			||||||
 | 
					            onPressed: () {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (_formKey.currentState!.validate()) {
 | 
				
			||||||
 | 
					                _formKey.currentState!.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                _save(context);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: Form(
 | 
				
			||||||
 | 
					        key: _formKey,
 | 
				
			||||||
 | 
					        child: SingleChildScrollView(
 | 
				
			||||||
 | 
					          child: Column(
 | 
				
			||||||
 | 
					            mainAxisAlignment: MainAxisAlignment.start,
 | 
				
			||||||
 | 
					            mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					            children: _buildForm(),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          padding: EdgeInsets.all(16),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								lib/app_colors.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/app_colors.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'dart:ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
 | 
				
			||||||
 | 
					const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Color COLOR_CLICK = Color.fromRGBO(175, 150, 100, 0.9);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Color COLOR_BLUE = Color.fromRGBO(0, 0, 250, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Color COLOR_STAR = Color.fromRGBO(250, 250, 100, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
 | 
				
			||||||
 | 
					const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1);
 | 
				
			||||||
 | 
					const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1);
 | 
				
			||||||
 | 
					const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Color COLOR_SELECTED = Color.fromRGBO(0, 0, 0, 0.05);
 | 
				
			||||||
@@ -271,8 +271,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  final InvenTreeStockItem item;
 | 
					  final InvenTreeStockItem item;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  StockItemBarcodeAssignmentHandler(this.item) {
 | 
					  StockItemBarcodeAssignmentHandler(this.item);
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
 | 
					  String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -126,8 +126,7 @@ class InvenTreeModel {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Return the API detail endpoint for this Model object
 | 
					  // Return the API detail endpoint for this Model object
 | 
				
			||||||
  String get url => "${URL}/${pk}/";
 | 
					  String get url => "${URL}/${pk}/".replaceAll("//", "/");
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Search this Model type in the database
 | 
					  // Search this Model type in the database
 | 
				
			||||||
  Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters = const {}}) async {
 | 
					  Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters = const {}}) async {
 | 
				
			||||||
@@ -277,8 +276,6 @@ class InvenTreeModel {
 | 
				
			|||||||
      params[key] = filters[key] ?? '';
 | 
					      params[key] = filters[key] ?? '';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    print("LIST: $URL ${params.toString()}");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var response = await api.get(URL, params: params);
 | 
					    var response = await api.get(URL, params: params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // A list of "InvenTreeModel" items
 | 
					    // A list of "InvenTreeModel" items
 | 
				
			||||||
@@ -288,18 +285,22 @@ class InvenTreeModel {
 | 
				
			|||||||
      return results;
 | 
					      return results;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO - handle possible error cases:
 | 
					    dynamic data;
 | 
				
			||||||
    // - No data receieved
 | 
					 | 
				
			||||||
    // - Data is not a list of maps
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (var d in response.data) {
 | 
					    if (response.data is List) {
 | 
				
			||||||
 | 
					      data = response.data;
 | 
				
			||||||
 | 
					    } else if (response.data.containsKey('results')) {
 | 
				
			||||||
 | 
					      data = response.data['results'];
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      data = [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var d in data) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Create a new object (of the current class type
 | 
					      // Create a new object (of the current class type
 | 
				
			||||||
      InvenTreeModel obj = createFromJson(d);
 | 
					      InvenTreeModel obj = createFromJson(d);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (obj != null) {
 | 
					      results.add(obj);
 | 
				
			||||||
        results.add(obj);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return results;
 | 
					    return results;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,6 +60,8 @@ class InvenTreeStockItem extends InvenTreeModel {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String statusLabel(BuildContext context) {
 | 
					  String statusLabel(BuildContext context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO: Delete me - The translated status values are provided by the API!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch (status) {
 | 
					    switch (status) {
 | 
				
			||||||
      case OK:
 | 
					      case OK:
 | 
				
			||||||
        return L10().ok;
 | 
					        return L10().ok;
 | 
				
			||||||
@@ -220,6 +222,15 @@ class InvenTreeStockItem extends InvenTreeModel {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  int get partId => jsondata['part'] ?? -1;
 | 
					  int get partId => jsondata['part'] ?? -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get purchasePrice => jsondata['purchase_price'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get hasPurchasePrice {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    String pp = purchasePrice;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return pp.isNotEmpty && pp.trim() != "-";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
 | 
					  int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Date of last update
 | 
					  // Date of last update
 | 
				
			||||||
@@ -476,14 +487,7 @@ class InvenTreeStockItem extends InvenTreeModel {
 | 
				
			|||||||
      expectedStatusCode: 200
 | 
					      expectedStatusCode: 200
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    print("Adjustment completed!");
 | 
					    return response.isValid();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (response == null) {
 | 
					 | 
				
			||||||
      return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Stock adjustment succeeded!
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
 | 
					  Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								lib/l10n
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								lib/l10n
									
									
									
									
									
								
							 Submodule lib/l10n updated: af4cd9026a...46d08c9cc0
									
								
							@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
import 'package:inventree/widget/dialogs.dart';
 | 
					import 'package:inventree/widget/dialogs.dart';
 | 
				
			||||||
import 'package:inventree/widget/fields.dart';
 | 
					import 'package:inventree/widget/fields.dart';
 | 
				
			||||||
import 'package:inventree/widget/spinner.dart';
 | 
					import 'package:inventree/widget/spinner.dart';
 | 
				
			||||||
@@ -55,7 +56,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
 | 
				
			|||||||
      key: _addProfileKey,
 | 
					      key: _addProfileKey,
 | 
				
			||||||
      callback: () {
 | 
					      callback: () {
 | 
				
			||||||
          if (createNew) {
 | 
					          if (createNew) {
 | 
				
			||||||
            // TODO - create the new profile...
 | 
					
 | 
				
			||||||
            UserProfile profile = UserProfile(
 | 
					            UserProfile profile = UserProfile(
 | 
				
			||||||
              name: _name,
 | 
					              name: _name,
 | 
				
			||||||
              server: _server,
 | 
					              server: _server,
 | 
				
			||||||
@@ -219,7 +220,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
 | 
				
			|||||||
    if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
 | 
					    if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
 | 
				
			||||||
      return FaIcon(
 | 
					      return FaIcon(
 | 
				
			||||||
        FontAwesomeIcons.questionCircle,
 | 
					        FontAwesomeIcons.questionCircle,
 | 
				
			||||||
        color: Color.fromRGBO(250, 150, 50, 1)
 | 
					        color: COLOR_WARNING
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -227,17 +228,17 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
 | 
				
			|||||||
    if (InvenTreeAPI().isConnected()) {
 | 
					    if (InvenTreeAPI().isConnected()) {
 | 
				
			||||||
      return FaIcon(
 | 
					      return FaIcon(
 | 
				
			||||||
        FontAwesomeIcons.checkCircle,
 | 
					        FontAwesomeIcons.checkCircle,
 | 
				
			||||||
        color: Color.fromRGBO(50, 250, 50, 1)
 | 
					        color: COLOR_SUCCESS
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else if (InvenTreeAPI().isConnecting()) {
 | 
					    } else if (InvenTreeAPI().isConnecting()) {
 | 
				
			||||||
      return Spinner(
 | 
					      return Spinner(
 | 
				
			||||||
        icon: FontAwesomeIcons.spinner,
 | 
					        icon: FontAwesomeIcons.spinner,
 | 
				
			||||||
        color: Color.fromRGBO(50, 50, 250, 1),
 | 
					        color: COLOR_PROGRESS,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return FaIcon(
 | 
					      return FaIcon(
 | 
				
			||||||
        FontAwesomeIcons.timesCircle,
 | 
					        FontAwesomeIcons.timesCircle,
 | 
				
			||||||
        color: Color.fromRGBO(250, 50, 50, 1),
 | 
					        color: COLOR_DANGER,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -255,7 +256,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
 | 
				
			|||||||
          title: Text(
 | 
					          title: Text(
 | 
				
			||||||
            profile.name,
 | 
					            profile.name,
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          tileColor: profile.selected ? Color.fromRGBO(0, 0, 0, 0.05) : null,
 | 
					          tileColor: profile.selected ? COLOR_SELECTED : null,
 | 
				
			||||||
          subtitle: Text("${profile.server}"),
 | 
					          subtitle: Text("${profile.server}"),
 | 
				
			||||||
          trailing: _getProfileIcon(profile),
 | 
					          trailing: _getProfileIcon(profile),
 | 
				
			||||||
          onTap: () {
 | 
					          onTap: () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:inventree/api.dart';
 | 
					import 'package:inventree/api.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
import 'package:inventree/app_settings.dart';
 | 
					import 'package:inventree/app_settings.dart';
 | 
				
			||||||
import 'package:inventree/inventree/part.dart';
 | 
					import 'package:inventree/inventree/part.dart';
 | 
				
			||||||
import 'package:inventree/inventree/sentry.dart';
 | 
					import 'package:inventree/inventree/sentry.dart';
 | 
				
			||||||
@@ -22,6 +23,8 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
					import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
				
			||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
 | 
					import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '../api_form.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CategoryDisplayWidget extends StatefulWidget {
 | 
					class CategoryDisplayWidget extends StatefulWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
 | 
					  CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
 | 
				
			||||||
@@ -35,7 +38,6 @@ class CategoryDisplayWidget extends StatefulWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
 | 
					class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final _editCategoryKey = GlobalKey<FormState>();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String getAppBarTitle(BuildContext context) => L10().partCategory;
 | 
					  String getAppBarTitle(BuildContext context) => L10().partCategory;
 | 
				
			||||||
@@ -71,7 +73,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
 | 
				
			|||||||
        IconButton(
 | 
					        IconButton(
 | 
				
			||||||
          icon: FaIcon(FontAwesomeIcons.edit),
 | 
					          icon: FaIcon(FontAwesomeIcons.edit),
 | 
				
			||||||
          tooltip: L10().edit,
 | 
					          tooltip: L10().edit,
 | 
				
			||||||
          onPressed: _editCategoryDialog,
 | 
					          onPressed: () {
 | 
				
			||||||
 | 
					            _editCategoryDialog(context);
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -80,49 +84,26 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _editCategory(Map<String, String> values) async {
 | 
					  void _editCategoryDialog(BuildContext context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final bool result = await category!.update(values: values);
 | 
					    final _cat = category;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    showSnackIcon(
 | 
					 | 
				
			||||||
      result ? "Category edited" : "Category editing failed",
 | 
					 | 
				
			||||||
      success: result
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    refresh();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void _editCategoryDialog() {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Cannot edit top-level category
 | 
					    // Cannot edit top-level category
 | 
				
			||||||
    if (category == null) {
 | 
					    if (_cat == null) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var _name;
 | 
					    launchApiForm(
 | 
				
			||||||
    var _description;
 | 
					      context,
 | 
				
			||||||
 | 
					 | 
				
			||||||
    showFormDialog(
 | 
					 | 
				
			||||||
      L10().editCategory,
 | 
					      L10().editCategory,
 | 
				
			||||||
      key: _editCategoryKey,
 | 
					      _cat.url,
 | 
				
			||||||
      callback: () {
 | 
					      {
 | 
				
			||||||
        _editCategory({
 | 
					        "name": {},
 | 
				
			||||||
          "name": _name,
 | 
					        "description": {},
 | 
				
			||||||
          "description": _description
 | 
					        "parent": {},
 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      fields: <Widget>[
 | 
					      modelData: _cat.jsondata,
 | 
				
			||||||
        StringField(
 | 
					      onSuccess: refresh,
 | 
				
			||||||
          label: L10().name,
 | 
					 | 
				
			||||||
          initial: category?.name,
 | 
					 | 
				
			||||||
          onSaved: (value) => _name = value
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        StringField(
 | 
					 | 
				
			||||||
          label: L10().description,
 | 
					 | 
				
			||||||
          initial: category?.description,
 | 
					 | 
				
			||||||
          onSaved: (value) => _description = value
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -186,7 +167,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
 | 
				
			|||||||
            ListTile(
 | 
					            ListTile(
 | 
				
			||||||
              title: Text(L10().parentCategory),
 | 
					              title: Text(L10().parentCategory),
 | 
				
			||||||
              subtitle: Text("${category?.parentpathstring}"),
 | 
					              subtitle: Text("${category?.parentpathstring}"),
 | 
				
			||||||
              leading: FaIcon(FontAwesomeIcons.levelUpAlt),
 | 
					              leading: FaIcon(
 | 
				
			||||||
 | 
					                FontAwesomeIcons.levelUpAlt,
 | 
				
			||||||
 | 
					                color: COLOR_CLICK,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
                if (category == null || ((category?.parentId ?? 0) < 0)) {
 | 
					                if (category == null || ((category?.parentId ?? 0) < 0)) {
 | 
				
			||||||
                  Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
 | 
					                  Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:image_picker/image_picker.dart';
 | 
					import 'package:image_picker/image_picker.dart';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:inventree/l10.dart';
 | 
					import 'package:inventree/l10.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
@@ -92,23 +91,28 @@ class ImagePickerField extends FormField<File> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CheckBoxField extends FormField<bool> {
 | 
					class CheckBoxField extends FormField<bool> {
 | 
				
			||||||
  CheckBoxField({String? label, String? hint, bool initial = false, Function(bool?)? onSaved}) :
 | 
					  CheckBoxField({
 | 
				
			||||||
 | 
					      String? label, bool initial = false, Function(bool?)? onSaved,
 | 
				
			||||||
 | 
					      TextStyle? labelStyle,
 | 
				
			||||||
 | 
					      String? helperText,
 | 
				
			||||||
 | 
					      TextStyle? helperStyle,
 | 
				
			||||||
 | 
					  }) :
 | 
				
			||||||
      super(
 | 
					      super(
 | 
				
			||||||
        onSaved: onSaved,
 | 
					        onSaved: onSaved,
 | 
				
			||||||
        initialValue: initial,
 | 
					        initialValue: initial,
 | 
				
			||||||
        builder: (FormFieldState<bool> state) {
 | 
					        builder: (FormFieldState<bool> state) {
 | 
				
			||||||
          return CheckboxListTile(
 | 
					          return CheckboxListTile(
 | 
				
			||||||
            //dense: state.hasError,
 | 
					            //dense: state.hasError,
 | 
				
			||||||
            title: label == null ? null : Text(label),
 | 
					            title: label != null ? Text(label, style: labelStyle) : null,
 | 
				
			||||||
            value: state.value,
 | 
					            value: state.value,
 | 
				
			||||||
            onChanged: state.didChange,
 | 
					            onChanged: state.didChange,
 | 
				
			||||||
            subtitle: hint == null ? null : Text(hint),
 | 
					            subtitle: helperText != null ? Text(helperText, style: helperStyle) : null,
 | 
				
			||||||
 | 
					            contentPadding: EdgeInsets.zero,
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class StringField extends TextFormField {
 | 
					class StringField extends TextFormField {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) :
 | 
					  StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) :
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
import 'package:inventree/user_profile.dart';
 | 
					import 'package:inventree/user_profile.dart';
 | 
				
			||||||
import 'package:flutter/cupertino.dart';
 | 
					import 'package:flutter/cupertino.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
@@ -19,6 +20,7 @@ import 'package:inventree/widget/spinner.dart';
 | 
				
			|||||||
import 'package:inventree/widget/drawer.dart';
 | 
					import 'package:inventree/widget/drawer.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class InvenTreeHomePage extends StatefulWidget {
 | 
					class InvenTreeHomePage extends StatefulWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  InvenTreeHomePage({Key? key}) : super(key: key);
 | 
					  InvenTreeHomePage({Key? key}) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -130,7 +132,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
 | 
				
			|||||||
        leading: FaIcon(FontAwesomeIcons.server),
 | 
					        leading: FaIcon(FontAwesomeIcons.server),
 | 
				
			||||||
        trailing: FaIcon(
 | 
					        trailing: FaIcon(
 | 
				
			||||||
          FontAwesomeIcons.user,
 | 
					          FontAwesomeIcons.user,
 | 
				
			||||||
          color: Color.fromRGBO(250, 50, 50, 1),
 | 
					          color: COLOR_DANGER,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
          _selectProfile();
 | 
					          _selectProfile();
 | 
				
			||||||
@@ -146,7 +148,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
 | 
				
			|||||||
        leading: FaIcon(FontAwesomeIcons.server),
 | 
					        leading: FaIcon(FontAwesomeIcons.server),
 | 
				
			||||||
        trailing: Spinner(
 | 
					        trailing: Spinner(
 | 
				
			||||||
          icon: FontAwesomeIcons.spinner,
 | 
					          icon: FontAwesomeIcons.spinner,
 | 
				
			||||||
          color: Color.fromRGBO(50, 50, 250, 1),
 | 
					          color: COLOR_PROGRESS,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
          _selectProfile();
 | 
					          _selectProfile();
 | 
				
			||||||
@@ -159,7 +161,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
 | 
				
			|||||||
        leading: FaIcon(FontAwesomeIcons.server),
 | 
					        leading: FaIcon(FontAwesomeIcons.server),
 | 
				
			||||||
        trailing: FaIcon(
 | 
					        trailing: FaIcon(
 | 
				
			||||||
          FontAwesomeIcons.checkCircle,
 | 
					          FontAwesomeIcons.checkCircle,
 | 
				
			||||||
          color: Color.fromRGBO(50, 250, 50, 1)
 | 
					          color: COLOR_SUCCESS
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
          _selectProfile();
 | 
					          _selectProfile();
 | 
				
			||||||
@@ -172,7 +174,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
 | 
				
			|||||||
        leading: FaIcon(FontAwesomeIcons.server),
 | 
					        leading: FaIcon(FontAwesomeIcons.server),
 | 
				
			||||||
        trailing: FaIcon(
 | 
					        trailing: FaIcon(
 | 
				
			||||||
          FontAwesomeIcons.timesCircle,
 | 
					          FontAwesomeIcons.timesCircle,
 | 
				
			||||||
          color: Color.fromRGBO(250, 50, 50, 1),
 | 
					          color: COLOR_DANGER,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
          _selectProfile();
 | 
					          _selectProfile();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,6 @@
 | 
				
			|||||||
import 'package:inventree/api.dart';
 | 
					import 'package:inventree/api.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/api_form.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
import 'package:inventree/app_settings.dart';
 | 
					import 'package:inventree/app_settings.dart';
 | 
				
			||||||
import 'package:inventree/barcode.dart';
 | 
					import 'package:inventree/barcode.dart';
 | 
				
			||||||
import 'package:inventree/inventree/sentry.dart';
 | 
					import 'package:inventree/inventree/sentry.dart';
 | 
				
			||||||
@@ -71,7 +73,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
 | 
				
			|||||||
        IconButton(
 | 
					        IconButton(
 | 
				
			||||||
          icon: FaIcon(FontAwesomeIcons.edit),
 | 
					          icon: FaIcon(FontAwesomeIcons.edit),
 | 
				
			||||||
          tooltip: L10().edit,
 | 
					          tooltip: L10().edit,
 | 
				
			||||||
          onPressed: _editLocationDialog,
 | 
					          onPressed: () { _editLocationDialog(context); },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -79,23 +81,27 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
 | 
				
			|||||||
    return actions;
 | 
					    return actions;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _editLocation(Map<String, String> values) async {
 | 
					  void _editLocationDialog(BuildContext context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    bool result = false;
 | 
					    final _loc = location;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (location != null) {
 | 
					    if (_loc == null) {
 | 
				
			||||||
      result = await location!.update(values: values);
 | 
					      return;
 | 
				
			||||||
 | 
					 | 
				
			||||||
      showSnackIcon(
 | 
					 | 
				
			||||||
          result ? "Location edited" : "Location editing failed",
 | 
					 | 
				
			||||||
          success: result
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    refresh();
 | 
					    launchApiForm(
 | 
				
			||||||
  }
 | 
					      context,
 | 
				
			||||||
 | 
					      L10().editLocation,
 | 
				
			||||||
 | 
					      _loc.url,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "name": {},
 | 
				
			||||||
 | 
					        "description": {},
 | 
				
			||||||
 | 
					        "parent": {},
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      modelData: _loc.jsondata,
 | 
				
			||||||
 | 
					      onSuccess: refresh
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _editLocationDialog() {
 | 
					 | 
				
			||||||
    // Values which an be edited
 | 
					    // Values which an be edited
 | 
				
			||||||
    var _name;
 | 
					    var _name;
 | 
				
			||||||
    var _description;
 | 
					    var _description;
 | 
				
			||||||
@@ -103,28 +109,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
 | 
				
			|||||||
    if (location == null) {
 | 
					    if (location == null) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    showFormDialog(L10().editLocation,
 | 
					 | 
				
			||||||
      key: _editLocationKey,
 | 
					 | 
				
			||||||
      callback: () {
 | 
					 | 
				
			||||||
        _editLocation({
 | 
					 | 
				
			||||||
          "name": _name,
 | 
					 | 
				
			||||||
          "description": _description
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      fields: <Widget> [
 | 
					 | 
				
			||||||
        StringField(
 | 
					 | 
				
			||||||
          label: L10().name,
 | 
					 | 
				
			||||||
          initial: location?.name ?? '',
 | 
					 | 
				
			||||||
          onSaved: (value) => _name = value,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        StringField(
 | 
					 | 
				
			||||||
          label: L10().description,
 | 
					 | 
				
			||||||
          initial: location?.description ?? '',
 | 
					 | 
				
			||||||
          onSaved: (value) => _description = value,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _LocationDisplayState(this.location);
 | 
					  _LocationDisplayState(this.location);
 | 
				
			||||||
@@ -193,7 +177,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
 | 
				
			|||||||
            ListTile(
 | 
					            ListTile(
 | 
				
			||||||
              title: Text(L10().parentCategory),
 | 
					              title: Text(L10().parentCategory),
 | 
				
			||||||
              subtitle: Text("${location!.parentpathstring}"),
 | 
					              subtitle: Text("${location!.parentpathstring}"),
 | 
				
			||||||
              leading: FaIcon(FontAwesomeIcons.levelUpAlt),
 | 
					              leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                int parent = location?.parentId ?? -1;
 | 
					                int parent = location?.parentId ?? -1;
 | 
				
			||||||
@@ -319,7 +303,7 @@ List<Widget> detailTiles() {
 | 
				
			|||||||
        tiles.add(
 | 
					        tiles.add(
 | 
				
			||||||
            ListTile(
 | 
					            ListTile(
 | 
				
			||||||
              title: Text(L10().barcodeScanInItems),
 | 
					              title: Text(L10().barcodeScanInItems),
 | 
				
			||||||
              leading: FaIcon(FontAwesomeIcons.exchangeAlt),
 | 
					              leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
 | 
				
			||||||
              trailing: FaIcon(FontAwesomeIcons.qrcode),
 | 
					              trailing: FaIcon(FontAwesomeIcons.qrcode),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,19 +21,14 @@ class PaginatedSearchWidget extends StatelessWidget {
 | 
				
			|||||||
      leading: GestureDetector(
 | 
					      leading: GestureDetector(
 | 
				
			||||||
        child: FaIcon(controller.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace),
 | 
					        child: FaIcon(controller.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace),
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
          if (onChanged != null) {
 | 
					          controller.clear();
 | 
				
			||||||
            controller.clear();
 | 
					          onChanged();
 | 
				
			||||||
            onChanged();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      title: TextFormField(
 | 
					      title: TextFormField(
 | 
				
			||||||
        controller: controller,
 | 
					        controller: controller,
 | 
				
			||||||
        onChanged: (value) {
 | 
					        onChanged: (value) {
 | 
				
			||||||
 | 
					          onChanged();
 | 
				
			||||||
          if (onChanged != null) {
 | 
					 | 
				
			||||||
            onChanged();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        decoration: InputDecoration(
 | 
					        decoration: InputDecoration(
 | 
				
			||||||
          hintText: L10().search,
 | 
					          hintText: L10().search,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,16 @@
 | 
				
			|||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:inventree/widget/part_notes.dart';
 | 
					 | 
				
			||||||
import 'package:inventree/widget/progress.dart';
 | 
					 | 
				
			||||||
import 'package:inventree/widget/snacks.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/cupertino.dart';
 | 
					import 'package:flutter/cupertino.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:inventree/l10.dart';
 | 
					 | 
				
			||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
					import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:inventree/l10.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/api_form.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/widget/part_notes.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/widget/progress.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/widget/snacks.dart';
 | 
				
			||||||
import 'package:inventree/inventree/part.dart';
 | 
					import 'package:inventree/inventree/part.dart';
 | 
				
			||||||
import 'package:inventree/widget/full_screen_image.dart';
 | 
					import 'package:inventree/widget/full_screen_image.dart';
 | 
				
			||||||
import 'package:inventree/widget/category_display.dart';
 | 
					import 'package:inventree/widget/category_display.dart';
 | 
				
			||||||
@@ -59,7 +61,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
        IconButton(
 | 
					        IconButton(
 | 
				
			||||||
          icon: FaIcon(FontAwesomeIcons.edit),
 | 
					          icon: FaIcon(FontAwesomeIcons.edit),
 | 
				
			||||||
          tooltip: L10().edit,
 | 
					          tooltip: L10().edit,
 | 
				
			||||||
          onPressed: _editPartDialog,
 | 
					          onPressed: () {
 | 
				
			||||||
 | 
					            _editPartDialog(context);
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -169,58 +173,36 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _editPartDialog() {
 | 
					  void _editPartDialog(BuildContext context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Values which can be edited
 | 
					    launchApiForm(
 | 
				
			||||||
    var _name;
 | 
					        context,
 | 
				
			||||||
    var _description;
 | 
					        L10().editPart,
 | 
				
			||||||
    var _ipn;
 | 
					        part.url,
 | 
				
			||||||
    var _keywords;
 | 
					        {
 | 
				
			||||||
    var _link;
 | 
					          "name": {},
 | 
				
			||||||
 | 
					          "description": {},
 | 
				
			||||||
 | 
					          "IPN": {},
 | 
				
			||||||
 | 
					          "revision": {},
 | 
				
			||||||
 | 
					          "keywords": {},
 | 
				
			||||||
 | 
					          "link": {},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    showFormDialog(L10().editPart,
 | 
					          "category": {
 | 
				
			||||||
      key: _editPartKey,
 | 
					          },
 | 
				
			||||||
      callback: () {
 | 
					
 | 
				
			||||||
        _savePart({
 | 
					          // Checkbox fields
 | 
				
			||||||
          "name": _name,
 | 
					          "active": {},
 | 
				
			||||||
          "description": _description,
 | 
					          "assembly": {},
 | 
				
			||||||
          "IPN": _ipn,
 | 
					          "component": {},
 | 
				
			||||||
          "keywords": _keywords,
 | 
					          "purchaseable": {},
 | 
				
			||||||
          "link": _link
 | 
					          "salable": {},
 | 
				
			||||||
        });
 | 
					          "trackable": {},
 | 
				
			||||||
      },
 | 
					          "is_template": {},
 | 
				
			||||||
      fields: <Widget>[
 | 
					          "virtual": {},
 | 
				
			||||||
        StringField(
 | 
					        },
 | 
				
			||||||
          label: L10().name,
 | 
					        modelData: part.jsondata,
 | 
				
			||||||
          initial: part.name,
 | 
					        onSuccess: refresh,
 | 
				
			||||||
          onSaved: (value) => _name = value,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        StringField(
 | 
					 | 
				
			||||||
          label: L10().description,
 | 
					 | 
				
			||||||
          initial: part.description,
 | 
					 | 
				
			||||||
          onSaved: (value) => _description = value,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        StringField(
 | 
					 | 
				
			||||||
          label: L10().internalPartNumber,
 | 
					 | 
				
			||||||
          initial: part.IPN,
 | 
					 | 
				
			||||||
          allowEmpty: true,
 | 
					 | 
				
			||||||
          onSaved: (value) => _ipn = value,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        StringField(
 | 
					 | 
				
			||||||
          label: L10().keywords,
 | 
					 | 
				
			||||||
          initial: part.keywords,
 | 
					 | 
				
			||||||
          allowEmpty: true,
 | 
					 | 
				
			||||||
          onSaved: (value) => _keywords = value,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        StringField(
 | 
					 | 
				
			||||||
          label: L10().link,
 | 
					 | 
				
			||||||
          initial: part.link,
 | 
					 | 
				
			||||||
          allowEmpty: true,
 | 
					 | 
				
			||||||
          onSaved: (value) => _link = value
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Widget headerTile() {
 | 
					  Widget headerTile() {
 | 
				
			||||||
@@ -230,7 +212,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
          subtitle: Text("${part.description}"),
 | 
					          subtitle: Text("${part.description}"),
 | 
				
			||||||
          trailing: IconButton(
 | 
					          trailing: IconButton(
 | 
				
			||||||
            icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star,
 | 
					            icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star,
 | 
				
			||||||
              color: part.starred ? Color.fromRGBO(250, 250, 100, 1) : null,
 | 
					              color: part.starred ? COLOR_STAR : null,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            onPressed: _toggleStar,
 | 
					            onPressed: _toggleStar,
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@@ -264,13 +246,36 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
      return tiles;
 | 
					      return tiles;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!part.isActive) {
 | 
				
			||||||
 | 
					      tiles.add(
 | 
				
			||||||
 | 
					        ListTile(
 | 
				
			||||||
 | 
					          title: Text(
 | 
				
			||||||
 | 
					              L10().inactive,
 | 
				
			||||||
 | 
					              style: TextStyle(
 | 
				
			||||||
 | 
					                color: COLOR_DANGER
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          subtitle: Text(
 | 
				
			||||||
 | 
					            L10().inactiveDetail,
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					              color: COLOR_DANGER
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          leading: FaIcon(
 | 
				
			||||||
 | 
					              FontAwesomeIcons.exclamationCircle,
 | 
				
			||||||
 | 
					              color: COLOR_DANGER
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Category information
 | 
					    // Category information
 | 
				
			||||||
    if (part.categoryName.isNotEmpty) {
 | 
					    if (part.categoryName.isNotEmpty) {
 | 
				
			||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
            title: Text(L10().partCategory),
 | 
					            title: Text(L10().partCategory),
 | 
				
			||||||
            subtitle: Text("${part.categoryName}"),
 | 
					            subtitle: Text("${part.categoryName}"),
 | 
				
			||||||
            leading: FaIcon(FontAwesomeIcons.sitemap),
 | 
					            leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
 | 
				
			||||||
            onTap: () {
 | 
					            onTap: () {
 | 
				
			||||||
              if (part.categoryId > 0) {
 | 
					              if (part.categoryId > 0) {
 | 
				
			||||||
                InvenTreePartCategory().get(part.categoryId).then((var cat) {
 | 
					                InvenTreePartCategory().get(part.categoryId).then((var cat) {
 | 
				
			||||||
@@ -289,7 +294,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text(L10().partCategory),
 | 
					          title: Text(L10().partCategory),
 | 
				
			||||||
          subtitle: Text(L10().partCategoryTopLevel),
 | 
					          subtitle: Text(L10().partCategoryTopLevel),
 | 
				
			||||||
          leading: FaIcon(FontAwesomeIcons.sitemap),
 | 
					          leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
 | 
				
			||||||
          onTap: () {
 | 
					          onTap: () {
 | 
				
			||||||
            Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
 | 
					            Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -301,7 +306,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
    tiles.add(
 | 
					    tiles.add(
 | 
				
			||||||
      ListTile(
 | 
					      ListTile(
 | 
				
			||||||
        title: Text(L10().stock),
 | 
					        title: Text(L10().stock),
 | 
				
			||||||
        leading: FaIcon(FontAwesomeIcons.boxes),
 | 
					        leading: FaIcon(FontAwesomeIcons.boxes, color: COLOR_CLICK),
 | 
				
			||||||
        trailing: Text("${part.inStockString}"),
 | 
					        trailing: Text("${part.inStockString}"),
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
          setState(() {
 | 
					          setState(() {
 | 
				
			||||||
@@ -387,8 +392,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
            title: Text("${part.link}"),
 | 
					            title: Text("${part.link}"),
 | 
				
			||||||
            leading: FaIcon(FontAwesomeIcons.link),
 | 
					            leading: FaIcon(FontAwesomeIcons.link, color: COLOR_CLICK),
 | 
				
			||||||
            trailing: FaIcon(FontAwesomeIcons.externalLinkAlt),
 | 
					 | 
				
			||||||
            onTap: () {
 | 
					            onTap: () {
 | 
				
			||||||
              part.openLink();
 | 
					              part.openLink();
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@@ -412,7 +416,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
            title: Text(L10().notes),
 | 
					            title: Text(L10().notes),
 | 
				
			||||||
            leading: FaIcon(FontAwesomeIcons.stickyNote),
 | 
					            leading: FaIcon(FontAwesomeIcons.stickyNote, color: COLOR_CLICK),
 | 
				
			||||||
            trailing: Text(""),
 | 
					            trailing: Text(""),
 | 
				
			||||||
            onTap: () {
 | 
					            onTap: () {
 | 
				
			||||||
              Navigator.push(
 | 
					              Navigator.push(
 | 
				
			||||||
@@ -539,4 +543,4 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 | 
				
			|||||||
  Widget getBody(BuildContext context) {
 | 
					  Widget getBody(BuildContext context) {
 | 
				
			||||||
    return getSelectedWidget(tabIndex);
 | 
					    return getSelectedWidget(tabIndex);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:flutter/cupertino.dart';
 | 
					import 'package:flutter/cupertino.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
					import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Spinner extends StatefulWidget {
 | 
					class Spinner extends StatefulWidget {
 | 
				
			||||||
  final IconData? icon;
 | 
					  final IconData? icon;
 | 
				
			||||||
@@ -9,7 +10,7 @@ class Spinner extends StatefulWidget {
 | 
				
			|||||||
  final Color color;
 | 
					  final Color color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const Spinner({
 | 
					  const Spinner({
 | 
				
			||||||
    this.color = const Color.fromRGBO(150, 150, 150, 1),
 | 
					    this.color = COLOR_GRAY_LIGHT,
 | 
				
			||||||
    Key? key,
 | 
					    Key? key,
 | 
				
			||||||
    @required this.icon,
 | 
					    @required this.icon,
 | 
				
			||||||
    this.duration = const Duration(milliseconds: 1800),
 | 
					    this.duration = const Duration(milliseconds: 1800),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,6 @@
 | 
				
			|||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
import 'package:inventree/barcode.dart';
 | 
					import 'package:inventree/barcode.dart';
 | 
				
			||||||
 | 
					import 'package:inventree/inventree/model.dart';
 | 
				
			||||||
import 'package:inventree/inventree/stock.dart';
 | 
					import 'package:inventree/inventree/stock.dart';
 | 
				
			||||||
import 'package:inventree/inventree/part.dart';
 | 
					import 'package:inventree/inventree/part.dart';
 | 
				
			||||||
import 'package:inventree/widget/dialogs.dart';
 | 
					import 'package:inventree/widget/dialogs.dart';
 | 
				
			||||||
@@ -17,9 +19,11 @@ import 'package:inventree/l10.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:inventree/api.dart';
 | 
					import 'package:inventree/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
 | 
					import 'package:dropdown_search/dropdown_search.dart';
 | 
				
			||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
					import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '../api_form.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockDetailWidget extends StatefulWidget {
 | 
					class StockDetailWidget extends StatefulWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  StockDetailWidget(this.item, {Key? key}) : super(key: key);
 | 
					  StockDetailWidget(this.item, {Key? key}) : super(key: key);
 | 
				
			||||||
@@ -49,20 +53,29 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  List<Widget> getAppBarActions(BuildContext context) {
 | 
					  List<Widget> getAppBarActions(BuildContext context) {
 | 
				
			||||||
    return <Widget>[
 | 
					
 | 
				
			||||||
      IconButton(
 | 
					    List<Widget> actions = [];
 | 
				
			||||||
        icon: FaIcon(FontAwesomeIcons.globe),
 | 
					
 | 
				
			||||||
        onPressed: _openInvenTreePage,
 | 
					    if (InvenTreeAPI().checkPermission('stock', 'view')) {
 | 
				
			||||||
      ),
 | 
					      actions.add(
 | 
				
			||||||
      // TODO: Hide the 'edit' button if the user does not have permission!!
 | 
					        IconButton(
 | 
				
			||||||
      /*
 | 
					          icon: FaIcon(FontAwesomeIcons.globe),
 | 
				
			||||||
      IconButton(
 | 
					          onPressed: _openInvenTreePage,
 | 
				
			||||||
        icon: FaIcon(FontAwesomeIcons.edit),
 | 
					        )
 | 
				
			||||||
        tooltip: L10().edit,
 | 
					      );
 | 
				
			||||||
        onPressed: _editPartDialog,
 | 
					    }
 | 
				
			||||||
      )
 | 
					
 | 
				
			||||||
       */
 | 
					    if (InvenTreeAPI().checkPermission('stock', 'change')) {
 | 
				
			||||||
    ];
 | 
					      actions.add(
 | 
				
			||||||
 | 
					          IconButton(
 | 
				
			||||||
 | 
					            icon: FaIcon(FontAwesomeIcons.edit),
 | 
				
			||||||
 | 
					            tooltip: L10().edit,
 | 
				
			||||||
 | 
					            onPressed: () { _editStockItem(context); },
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return actions;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _openInvenTreePage() async {
 | 
					  Future<void> _openInvenTreePage() async {
 | 
				
			||||||
@@ -95,6 +108,24 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
    await item.getTestResults();
 | 
					    await item.getTestResults();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _editStockItem(BuildContext context) async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    launchApiForm(
 | 
				
			||||||
 | 
					      context,
 | 
				
			||||||
 | 
					      L10().editItem,
 | 
				
			||||||
 | 
					      item.url,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "status": {},
 | 
				
			||||||
 | 
					        "batch": {},
 | 
				
			||||||
 | 
					        "packaging": {},
 | 
				
			||||||
 | 
					        "link": {},
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      modelData: item.jsondata,
 | 
				
			||||||
 | 
					      onSuccess: refresh
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _addStock() async {
 | 
					  void _addStock() async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    double quantity = double.parse(_quantityController.text);
 | 
					    double quantity = double.parse(_quantityController.text);
 | 
				
			||||||
@@ -241,7 +272,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _transferStock(InvenTreeStockLocation location) async {
 | 
					  void _transferStock(int locationId) async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
 | 
					    double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
 | 
				
			||||||
    String notes = _notesController.text;
 | 
					    String notes = _notesController.text;
 | 
				
			||||||
@@ -249,7 +280,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
    _quantityController.clear();
 | 
					    _quantityController.clear();
 | 
				
			||||||
    _notesController.clear();
 | 
					    _notesController.clear();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var result = await item.transferStock(location.pk, quantity: quantity, notes: notes);
 | 
					    var result = await item.transferStock(locationId, quantity: quantity, notes: notes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    refresh();
 | 
					    refresh();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -258,22 +289,22 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _transferStockDialog() async {
 | 
					  void _transferStockDialog(BuildContext context) async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var locations = await InvenTreeStockLocation().list();
 | 
					    var locations = await InvenTreeStockLocation().list();
 | 
				
			||||||
    final _selectedController = TextEditingController();
 | 
					    final _selectedController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    InvenTreeStockLocation? selectedLocation;
 | 
					    int? location_pk;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _quantityController.text = "${item.quantityString}";
 | 
					    _quantityController.text = "${item.quantityString}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    showFormDialog(L10().transferStock,
 | 
					    showFormDialog(L10().transferStock,
 | 
				
			||||||
        key: _moveStockKey,
 | 
					        key: _moveStockKey,
 | 
				
			||||||
        callback: () {
 | 
					        callback: () {
 | 
				
			||||||
          var _loc = selectedLocation;
 | 
					          var _pk = location_pk;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (_loc != null) {
 | 
					          if (_pk != null) {
 | 
				
			||||||
            _transferStock(_loc);
 | 
					            _transferStock(_pk);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        fields: <Widget>[
 | 
					        fields: <Widget>[
 | 
				
			||||||
@@ -282,47 +313,57 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
            controller: _quantityController,
 | 
					            controller: _quantityController,
 | 
				
			||||||
            max: item.quantity,
 | 
					            max: item.quantity,
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          TypeAheadFormField(
 | 
					          DropdownSearch<dynamic>(
 | 
				
			||||||
              textFieldConfiguration: TextFieldConfiguration(
 | 
					            mode: Mode.BOTTOM_SHEET,
 | 
				
			||||||
                  controller: _selectedController,
 | 
					            showSelectedItem: false,
 | 
				
			||||||
                  autofocus: true,
 | 
					            autoFocusSearchBox: true,
 | 
				
			||||||
                  decoration: InputDecoration(
 | 
					            selectedItem: null,
 | 
				
			||||||
                      hintText: L10().searchLocation,
 | 
					            errorBuilder: (context, entry, exception) {
 | 
				
			||||||
                      border: OutlineInputBorder()
 | 
					              print("entry: $entry");
 | 
				
			||||||
                  )
 | 
					              print(exception.toString());
 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              suggestionsCallback: (pattern) async {
 | 
					 | 
				
			||||||
                List<InvenTreeStockLocation> suggestions = [];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                for (var loc in locations) {
 | 
					              return Text(
 | 
				
			||||||
                  if (loc.matchAgainstString(pattern)) {
 | 
					                exception.toString(),
 | 
				
			||||||
                    suggestions.add(loc as InvenTreeStockLocation);
 | 
					                style: TextStyle(
 | 
				
			||||||
                  }
 | 
					                  fontSize: 10,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            onFind: (String filter) async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              Map<String, String> _filters = {
 | 
				
			||||||
 | 
					                "search": filter,
 | 
				
			||||||
 | 
					                "offset": "0",
 | 
				
			||||||
 | 
					                "limit": "25"
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              final List<InvenTreeModel> results = await InvenTreeStockLocation().list(filters: _filters);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              List<dynamic> items = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              for (InvenTreeModel loc in results) {
 | 
				
			||||||
 | 
					                if (loc is InvenTreeStockLocation) {
 | 
				
			||||||
 | 
					                  items.add(loc.jsondata);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					 | 
				
			||||||
                return suggestions;
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              validator: (value) {
 | 
					 | 
				
			||||||
                if (selectedLocation == null) {
 | 
					 | 
				
			||||||
                  return L10().selectLocation;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return null;
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              onSuggestionSelected: (suggestion) {
 | 
					 | 
				
			||||||
                selectedLocation = suggestion as InvenTreeStockLocation;
 | 
					 | 
				
			||||||
                _selectedController.text = selectedLocation!.pathstring;
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              onSaved: (value) {
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              itemBuilder: (context, suggestion) {
 | 
					 | 
				
			||||||
                var location = suggestion as InvenTreeStockLocation;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return ListTile(
 | 
					 | 
				
			||||||
                  title: Text("${location.pathstring}"),
 | 
					 | 
				
			||||||
                  subtitle: Text("${location.description}"),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return items;
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            label: L10().stockLocation,
 | 
				
			||||||
 | 
					            hint: L10().searchLocation,
 | 
				
			||||||
 | 
					            onChanged: null,
 | 
				
			||||||
 | 
					            itemAsString: (dynamic location) {
 | 
				
			||||||
 | 
					              return location['pathstring'];
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            onSaved: (dynamic location) {
 | 
				
			||||||
 | 
					              if (location == null) {
 | 
				
			||||||
 | 
					                location_pk = null;
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                location_pk = location['pk'];
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            isFilteredOnline: true,
 | 
				
			||||||
 | 
					            showSearchBox:  true,
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@@ -394,7 +435,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
            title: Text(L10().stockLocation),
 | 
					            title: Text(L10().stockLocation),
 | 
				
			||||||
            subtitle: Text("${item.locationPathString}"),
 | 
					            subtitle: Text("${item.locationPathString}"),
 | 
				
			||||||
            leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
 | 
					            leading: FaIcon(
 | 
				
			||||||
 | 
					              FontAwesomeIcons.mapMarkerAlt,
 | 
				
			||||||
 | 
					              color: COLOR_CLICK,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
            onTap: () {
 | 
					            onTap: () {
 | 
				
			||||||
              if (item.locationId > 0) {
 | 
					              if (item.locationId > 0) {
 | 
				
			||||||
                InvenTreeStockLocation().get(item.locationId).then((var loc) {
 | 
					                InvenTreeStockLocation().get(item.locationId).then((var loc) {
 | 
				
			||||||
@@ -463,9 +507,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text("${item.link}"),
 | 
					          title: Text("${item.link}"),
 | 
				
			||||||
          leading: FaIcon(FontAwesomeIcons.link),
 | 
					          leading: FaIcon(FontAwesomeIcons.link, color: COLOR_CLICK),
 | 
				
			||||||
          trailing: Text(""),
 | 
					          onTap: () {
 | 
				
			||||||
          onTap: null,
 | 
					            item.openLink();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -474,7 +519,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
              title: Text(L10().testResults),
 | 
					              title: Text(L10().testResults),
 | 
				
			||||||
              leading: FaIcon(FontAwesomeIcons.tasks),
 | 
					              leading: FaIcon(FontAwesomeIcons.tasks, color: COLOR_CLICK),
 | 
				
			||||||
              trailing: Text("${item.testResultCount}"),
 | 
					              trailing: Text("${item.testResultCount}"),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
                Navigator.push(
 | 
					                Navigator.push(
 | 
				
			||||||
@@ -489,6 +534,18 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (item.hasPurchasePrice) {
 | 
				
			||||||
 | 
					      tiles.add(
 | 
				
			||||||
 | 
					        ListTile(
 | 
				
			||||||
 | 
					          title: Text(L10().purchasePrice),
 | 
				
			||||||
 | 
					          leading: FaIcon(FontAwesomeIcons.dollarSign),
 | 
				
			||||||
 | 
					          trailing: Text(item.purchasePrice),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO - Is this stock item linked to a PurchaseOrder?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO - Re-enable stock item history display
 | 
					    // TODO - Re-enable stock item history display
 | 
				
			||||||
    if (false && item.trackingItemCount > 0) {
 | 
					    if (false && item.trackingItemCount > 0) {
 | 
				
			||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
@@ -510,8 +567,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text(L10().notes),
 | 
					          title: Text(L10().notes),
 | 
				
			||||||
          leading: FaIcon(FontAwesomeIcons.stickyNote),
 | 
					          leading: FaIcon(FontAwesomeIcons.stickyNote, color: COLOR_CLICK),
 | 
				
			||||||
          trailing: Text(""),
 | 
					 | 
				
			||||||
          onTap: () {
 | 
					          onTap: () {
 | 
				
			||||||
            Navigator.push(
 | 
					            Navigator.push(
 | 
				
			||||||
              context,
 | 
					              context,
 | 
				
			||||||
@@ -527,7 +583,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
    return tiles;
 | 
					    return tiles;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<Widget> actionTiles() {
 | 
					  List<Widget> actionTiles(BuildContext context) {
 | 
				
			||||||
    List<Widget> tiles = [];
 | 
					    List<Widget> tiles = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tiles.add(headerTile());
 | 
					    tiles.add(headerTile());
 | 
				
			||||||
@@ -554,7 +610,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
              title: Text(L10().countStock),
 | 
					              title: Text(L10().countStock),
 | 
				
			||||||
              leading: FaIcon(FontAwesomeIcons.checkCircle),
 | 
					              leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK),
 | 
				
			||||||
              onTap: _countStockDialog,
 | 
					              onTap: _countStockDialog,
 | 
				
			||||||
              trailing: Text(item.quantityString),
 | 
					              trailing: Text(item.quantityString),
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
@@ -563,7 +619,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
              title: Text(L10().removeStock),
 | 
					              title: Text(L10().removeStock),
 | 
				
			||||||
              leading: FaIcon(FontAwesomeIcons.minusCircle),
 | 
					              leading: FaIcon(FontAwesomeIcons.minusCircle, color: COLOR_CLICK),
 | 
				
			||||||
              onTap: _removeStockDialog,
 | 
					              onTap: _removeStockDialog,
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -571,7 +627,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
              title: Text(L10().addStock),
 | 
					              title: Text(L10().addStock),
 | 
				
			||||||
              leading: FaIcon(FontAwesomeIcons.plusCircle),
 | 
					              leading: FaIcon(FontAwesomeIcons.plusCircle, color: COLOR_CLICK),
 | 
				
			||||||
              onTap: _addStockDialog,
 | 
					              onTap: _addStockDialog,
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -580,8 +636,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
    tiles.add(
 | 
					    tiles.add(
 | 
				
			||||||
      ListTile(
 | 
					      ListTile(
 | 
				
			||||||
        title: Text(L10().transferStock),
 | 
					        title: Text(L10().transferStock),
 | 
				
			||||||
        leading: FaIcon(FontAwesomeIcons.exchangeAlt),
 | 
					        leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
 | 
				
			||||||
        onTap: _transferStockDialog,
 | 
					        onTap: () { _transferStockDialog(context); },
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -589,7 +645,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
    tiles.add(
 | 
					    tiles.add(
 | 
				
			||||||
      ListTile(
 | 
					      ListTile(
 | 
				
			||||||
        title: Text(L10().scanIntoLocation),
 | 
					        title: Text(L10().scanIntoLocation),
 | 
				
			||||||
        leading: FaIcon(FontAwesomeIcons.exchangeAlt),
 | 
					        leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
 | 
				
			||||||
        trailing: FaIcon(FontAwesomeIcons.qrcode),
 | 
					        trailing: FaIcon(FontAwesomeIcons.qrcode),
 | 
				
			||||||
        onTap: () {
 | 
					        onTap: () {
 | 
				
			||||||
          Navigator.push(
 | 
					          Navigator.push(
 | 
				
			||||||
@@ -607,7 +663,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text(L10().barcodeAssign),
 | 
					          title: Text(L10().barcodeAssign),
 | 
				
			||||||
          leading: FaIcon(FontAwesomeIcons.barcode),
 | 
					          leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
 | 
				
			||||||
          trailing: FaIcon(FontAwesomeIcons.qrcode),
 | 
					          trailing: FaIcon(FontAwesomeIcons.qrcode),
 | 
				
			||||||
          onTap: () {
 | 
					          onTap: () {
 | 
				
			||||||
            Navigator.push(
 | 
					            Navigator.push(
 | 
				
			||||||
@@ -623,7 +679,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
      tiles.add(
 | 
					      tiles.add(
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text(L10().barcodeUnassign),
 | 
					          title: Text(L10().barcodeUnassign),
 | 
				
			||||||
          leading: FaIcon(FontAwesomeIcons.barcode),
 | 
					          leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
 | 
				
			||||||
          onTap: () {
 | 
					          onTap: () {
 | 
				
			||||||
            _unassignBarcode(context);
 | 
					            _unassignBarcode(context);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@@ -665,7 +721,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
 | 
				
			|||||||
        return ListView(
 | 
					        return ListView(
 | 
				
			||||||
          children: ListTile.divideTiles(
 | 
					          children: ListTile.divideTiles(
 | 
				
			||||||
            context: context,
 | 
					            context: context,
 | 
				
			||||||
            tiles: actionTiles()
 | 
					            tiles: actionTiles(context)
 | 
				
			||||||
          ).toList()
 | 
					          ).toList()
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'package:inventree/app_colors.dart';
 | 
				
			||||||
import 'package:inventree/inventree/part.dart';
 | 
					import 'package:inventree/inventree/part.dart';
 | 
				
			||||||
import 'package:inventree/inventree/stock.dart';
 | 
					import 'package:inventree/inventree/stock.dart';
 | 
				
			||||||
import 'package:inventree/inventree/model.dart';
 | 
					import 'package:inventree/inventree/model.dart';
 | 
				
			||||||
@@ -84,7 +85,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
        CheckBoxField(
 | 
					        CheckBoxField(
 | 
				
			||||||
          label: L10().result,
 | 
					          label: L10().result,
 | 
				
			||||||
          hint: L10().testPassedOrFailed,
 | 
					          helperText: L10().testPassedOrFailed,
 | 
				
			||||||
          initial: true,
 | 
					          initial: true,
 | 
				
			||||||
          onSaved: (value) => _result = value ?? false,
 | 
					          onSaved: (value) => _result = value ?? false,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -207,7 +208,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
 | 
				
			|||||||
      String _value = "";
 | 
					      String _value = "";
 | 
				
			||||||
      String _notes = "";
 | 
					      String _notes = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: Color.fromRGBO(0, 0, 250, 1));
 | 
					      FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
 | 
				
			||||||
      bool _valueRequired = false;
 | 
					      bool _valueRequired = false;
 | 
				
			||||||
      bool _attachmentRequired = false;
 | 
					      bool _attachmentRequired = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -229,11 +230,11 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (_result == true) {
 | 
					      if (_result == true) {
 | 
				
			||||||
        _icon = FaIcon(FontAwesomeIcons.checkCircle,
 | 
					        _icon = FaIcon(FontAwesomeIcons.checkCircle,
 | 
				
			||||||
          color: Color.fromRGBO(0, 250, 0, 0.8)
 | 
					          color: COLOR_SUCCESS,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      } else if (_result == false) {
 | 
					      } else if (_result == false) {
 | 
				
			||||||
        _icon = FaIcon(FontAwesomeIcons.timesCircle,
 | 
					        _icon = FaIcon(FontAwesomeIcons.timesCircle,
 | 
				
			||||||
          color: Color.fromRGBO(250, 0, 0, 0.8)
 | 
					          color: COLOR_DANGER,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -127,6 +127,13 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.0.1"
 | 
					    version: "2.0.1"
 | 
				
			||||||
 | 
					  dropdown_search:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: dropdown_search
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.6.3"
 | 
				
			||||||
  fake_async:
 | 
					  fake_async:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -167,27 +174,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.2"
 | 
					    version: "3.1.2"
 | 
				
			||||||
  flutter_keyboard_visibility:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: flutter_keyboard_visibility
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "5.0.2"
 | 
					 | 
				
			||||||
  flutter_keyboard_visibility_platform_interface:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: flutter_keyboard_visibility_platform_interface
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "2.0.0"
 | 
					 | 
				
			||||||
  flutter_keyboard_visibility_web:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: flutter_keyboard_visibility_web
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "2.0.0"
 | 
					 | 
				
			||||||
  flutter_launcher_icons:
 | 
					  flutter_launcher_icons:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -226,13 +212,6 @@ packages:
 | 
				
			|||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
    source: sdk
 | 
					    source: sdk
 | 
				
			||||||
    version: "0.0.0"
 | 
					    version: "0.0.0"
 | 
				
			||||||
  flutter_typeahead:
 | 
					 | 
				
			||||||
    dependency: "direct main"
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: flutter_typeahead
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.1.3"
 | 
					 | 
				
			||||||
  flutter_web_plugins:
 | 
					  flutter_web_plugins:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ description: InvenTree stock management
 | 
				
			|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
 | 
					# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
 | 
				
			||||||
# Read more about iOS versioning at
 | 
					# Read more about iOS versioning at
 | 
				
			||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
version: 0.2.10+18
 | 
					version: 0.3.1+19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ">=2.12.0 <3.0.0"
 | 
					  sdk: ">=2.12.0 <3.0.0"
 | 
				
			||||||
@@ -30,7 +30,6 @@ dependencies:
 | 
				
			|||||||
  font_awesome_flutter: ^9.1.0            # FontAwesome icon set
 | 
					  font_awesome_flutter: ^9.1.0            # FontAwesome icon set
 | 
				
			||||||
  flutter_speed_dial: ^3.0.5              # FAB menu elements
 | 
					  flutter_speed_dial: ^3.0.5              # FAB menu elements
 | 
				
			||||||
  sentry_flutter: 5.0.0                   # Error reporting
 | 
					  sentry_flutter: 5.0.0                   # Error reporting
 | 
				
			||||||
  flutter_typeahead: ^3.1.0               # Auto-complete input field
 | 
					 | 
				
			||||||
  image_picker: ^0.8.0                    # Select or take photos
 | 
					  image_picker: ^0.8.0                    # Select or take photos
 | 
				
			||||||
  url_launcher: 6.0.0                     # Open link in system browser
 | 
					  url_launcher: 6.0.0                     # Open link in system browser
 | 
				
			||||||
  flutter_markdown: ^0.6.2                # Rendering markdown
 | 
					  flutter_markdown: ^0.6.2                # Rendering markdown
 | 
				
			||||||
@@ -40,6 +39,7 @@ dependencies:
 | 
				
			|||||||
  one_context: ^1.1.0                     # Dialogs without requiring context
 | 
					  one_context: ^1.1.0                     # Dialogs without requiring context
 | 
				
			||||||
  infinite_scroll_pagination: ^3.1.0      # Let the server do all the work!
 | 
					  infinite_scroll_pagination: ^3.1.0      # Let the server do all the work!
 | 
				
			||||||
  audioplayers: ^0.19.0                   # Play audio files
 | 
					  audioplayers: ^0.19.0                   # Play audio files
 | 
				
			||||||
 | 
					  dropdown_search: 0.6.3                  # Dropdown autocomplete form fields
 | 
				
			||||||
  path:
 | 
					  path:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user