2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-05-02 15:28:53 +00:00

Merge pull request #72 from SchrodingersGat/purchase-orders

Purchase orders
This commit is contained in:
Oliver 2021-10-05 21:23:56 +11:00 committed by GitHub
commit 91ec55967d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 3869 additions and 2517 deletions

81
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,81 @@
# Run flutter linting checks
name: test
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
lint:
runs-on: ubuntu-latest
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: '12.x'
- name: Setup Flutter
uses: subosito/flutter-action@v1
with:
flutter-version: '2.2.3'
- run: flutter pub get
- run: cp lib/dummy_dsn.dart lib/dsn.dart
- run: flutter analyze
- run: flutter test --coverage
#android:
# runs-on: macos-latest
#
# steps:
# - name: Checkout code
# uses: actions/checkout@v2
# with:
# submodules: recursive
# - name: Setup Java
# uses: actions/setup-java@v1
# with:
# java-version: '12.x'
# - name: Setup Flutter
# uses: subosito/flutter-action@v1
# with:
# flutter-version: '2.2.3'
# - name: Setup Gradle
# uses: gradle/gradle-build-action@v2
# with:
# gradle-version: 6.1.1
# - run: flutter pub get
# - run: cp lib/dummy_dsn.dart lib/dsn.dart
# - run: flutter build apk
#ios:
# runs-on: macos-latest
#
# steps:
# - name: Checkout code
# uses: actions/checkout@v2
# with:
# submodules: recursive
# - name: Setup Java
# uses: actions/setup-java@v1
# with:
# java-version: '12.x'
# - name: Setup Flutter
# uses: subosito/flutter-action@v1
# with:
# flutter-version: '2.2.3'
# - run: flutter pub get
# - run: cp lib/dummy_dsn.dart lib/dsn.dart
# - run: flutter build ios --release --no-codesign

2
.gitignore vendored
View File

@ -9,6 +9,8 @@
.history
.svn/
coverage/*
# Sentry API key
lib/dsn.dart

65
analysis_options.yaml Normal file
View File

@ -0,0 +1,65 @@
include: package:lint/analysis_options.yaml
analyzer:
exclude:
- [build/**]
- lib/generated/**
language:
strict-raw-types: true
strong-mode:
implicit-casts: false
linter:
rules:
# ------ Disable individual rules ----- #
# --- #
# Turn off what you don't like. #
# ------------------------------------- #
# Make constructors the first thing in every class
sort_constructors_first: true
prefer_double_quotes: true
prefer_final_locals: false
prefer_const_constructors: false
prefer_final_in_for_each: false
use_build_context_synchronously: false
avoid_redundant_argument_values: false
unnecessary_brace_in_string_interps: false
unnecessary_string_interpolations: false
prefer_interpolation_to_compose_strings: false
no_logic_in_create_state: false
parameter_assignments: false
non_constant_identifier_names: false
constant_identifier_names: false
package_prefixed_library_names: false
prefer_const_literals_to_create_immutables: false
avoid_print: false
avoid_positional_boolean_parameters: false
prefer_final_fields: false
sort_child_properties_last: false
directives_ordering: false
# Blindly follow the Flutter code style, which prefers types everywhere
always_specify_types: false
avoid_unnecessary_containers: false

View File

@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.0'
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

View File

@ -1,6 +1,15 @@
## InvenTree App Release Notes
---
### 0.5.0 - October 2021
---
- Display Purchase Order details
- Edit Purchase Order information
- Display Company details (supplier / manufacturer / customer)
- Edit Company information
- Fixed bug relating to stock transfer for parts with specified "units"
### 0.4.7 - September 2021
---

View File

@ -1,24 +1,25 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import "dart:async";
import "dart:convert";
import "dart:io";
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import "package:flutter/foundation.dart";
import "package:http/http.dart" as http;
import "package:intl/intl.dart";
import "package:inventree/app_colors.dart";
import 'package:open_file/open_file.dart';
import 'package:flutter/cupertino.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import "package:open_file/open_file.dart";
import "package:flutter/cupertino.dart";
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:flutter_cache_manager/flutter_cache_manager.dart";
import 'package:inventree/widget/dialogs.dart';
import 'package:inventree/l10.dart';
import 'package:inventree/inventree/sentry.dart';
import 'package:inventree/user_profile.dart';
import 'package:inventree/widget/snacks.dart';
import 'package:path_provider/path_provider.dart';
import "package:inventree/widget/dialogs.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/user_profile.dart";
import "package:inventree/widget/snacks.dart";
import "package:path_provider/path_provider.dart";
/*
@ -49,7 +50,32 @@ class APIResponse {
bool clientError() => (statusCode >= 400) && (statusCode < 500);
bool serverError() => (statusCode >= 500);
bool serverError() => statusCode >= 500;
bool isMap() {
return data != null && data is Map<String, dynamic>;
}
Map<String, dynamic> asMap() {
if (isMap()) {
return data as Map<String, dynamic>;
} else {
// Empty map
return {};
}
}
bool isList() {
return data != null && data is List<dynamic>;
}
List<dynamic> asList() {
if (isList()) {
return data as List<dynamic>;
} else {
return [];
}
}
}
@ -60,8 +86,6 @@ class APIResponse {
*/
class InvenTreeFileService extends FileService {
HttpClient? _client;
InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
_client = client ?? HttpClient();
@ -73,6 +97,8 @@ class InvenTreeFileService extends FileService {
}
}
HttpClient? _client;
@override
Future<FileServiceResponse> get(String url,
{Map<String, String>? headers}) async {
@ -107,6 +133,12 @@ class InvenTreeFileService extends FileService {
class InvenTreeAPI {
factory InvenTreeAPI() {
return _api;
}
InvenTreeAPI._internal();
// Minimum required API version for server
static const _minApiVersion = 7;
@ -132,11 +164,12 @@ class InvenTreeAPI {
String _makeUrl(String url) {
// Strip leading slash
if (url.startsWith('/')) {
if (url.startsWith("/")) {
url = url.substring(1, url.length);
}
url = url.replaceAll('//', '/');
// Prevent double-slash
url = url.replaceAll("//", "/");
return baseUrl + url;
}
@ -149,7 +182,7 @@ class InvenTreeAPI {
if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
return _makeUrl(endpoint);
} else {
return _makeUrl("/api/" + endpoint);
return _makeUrl("/api/${endpoint}");
}
}
@ -184,10 +217,10 @@ class InvenTreeAPI {
}
// Server instance information
String instance = '';
String instance = "";
// Server version information
String _version = '';
String _version = "";
// API version of the connected server
int _apiVersion = 1;
@ -209,15 +242,14 @@ class InvenTreeAPI {
}
// Ensure we only ever create a single instance of the API class
static final InvenTreeAPI _api = new InvenTreeAPI._internal();
static final InvenTreeAPI _api = InvenTreeAPI._internal();
factory InvenTreeAPI() {
return _api;
bool supportPoReceive() {
// API endpoint for receiving purchase order line items was introduced in v12
return _apiVersion >= 12;
}
InvenTreeAPI._internal();
/*
* Connect to the remote InvenTree server:
*
@ -239,15 +271,15 @@ class InvenTreeAPI {
if (address.isEmpty || username.isEmpty || password.isEmpty) {
showSnackIcon(
"Incomplete profile details",
L10().incompleteDetails,
icon: FontAwesomeIcons.exclamationCircle,
success: false
);
return false;
}
if (!address.endsWith('/')) {
address = address + '/';
if (!address.endsWith("/")) {
address = address + "/";
}
/* TODO: Better URL validation
* - If not a valid URL, return error
@ -267,8 +299,10 @@ class InvenTreeAPI {
return false;
}
var data = response.asMap();
// We expect certain response from the server
if (response.data == null || !response.data.containsKey("server") || !response.data.containsKey("version") || !response.data.containsKey("instance")) {
if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) {
showServerError(
L10().missingData,
@ -279,11 +313,11 @@ class InvenTreeAPI {
}
// Record server information
_version = response.data["version"];
instance = response.data['instance'] ?? '';
_version = (data["version"] ?? "") as String;
instance = (data["instance"] ?? "") as String;
// Default API version is 1 if not provided
_apiVersion = (response.data['apiVersion'] ?? 1) as int;
_apiVersion = (data["apiVersion"] ?? 1) as int;
if (_apiVersion < _minApiVersion) {
@ -332,7 +366,9 @@ class InvenTreeAPI {
return false;
}
if (response.data == null || !response.data.containsKey("token")) {
data = response.asMap();
if (!data.containsKey("token")) {
showServerError(
L10().tokenMissing,
L10().tokenMissingFromResponse,
@ -342,7 +378,7 @@ class InvenTreeAPI {
}
// Return the received token
_token = response.data["token"];
_token = (data["token"] ?? "") as String;
print("Received token - $_token");
// Request user role information
@ -358,7 +394,7 @@ class InvenTreeAPI {
_connected = false;
_connecting = false;
_token = '';
_token = "";
profile = null;
}
@ -405,7 +441,7 @@ class InvenTreeAPI {
// Next we request the permissions assigned to the current user
// Note: 2021-02-27 this "roles" feature for the API was just introduced.
// Any 'older' version of the server allows any API method for any logged in user!
// Any "older" version of the server allows any API method for any logged in user!
// We will return immediately, but request the user roles in the background
var response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
@ -414,9 +450,11 @@ class InvenTreeAPI {
return;
}
if (response.data.containsKey('roles')) {
var data = response.asMap();
if (data.containsKey("roles")) {
// Save a local copy of the user roles
roles = response.data['roles'];
roles = response.data["roles"] as Map<String, dynamic>;
}
}
@ -424,7 +462,7 @@ class InvenTreeAPI {
/*
* Check if the user has the given role.permission assigned
*e
* e.g. 'part', 'change'
* e.g. "part", "change"
*/
// If we do not have enough information, assume permission is allowed
@ -437,7 +475,7 @@ class InvenTreeAPI {
}
try {
List<String> perms = List.from(roles[role]);
List<String> perms = List.from(roles[role] as List<dynamic>);
return perms.contains(permission);
} catch (error, stackTrace) {
sentryReportError(error, stackTrace);
@ -447,19 +485,17 @@ class InvenTreeAPI {
// Perform a PATCH request
Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int? expectedStatusCode}) async {
var _body = Map<String, String>();
Future<APIResponse> patch(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode}) async {
// Copy across provided data
body.forEach((K, V) => _body[K] = V);
Map<String, dynamic> _body = body;
HttpClientRequest? request = await apiRequest(url, "PATCH");
if (request == null) {
// Return an "invalid" APIResponse
return new APIResponse(
return APIResponse(
url: url,
method: 'PATCH',
method: "PATCH",
error: "HttpClientRequest is null"
);
}
@ -503,7 +539,7 @@ class InvenTreeAPI {
HttpClientRequest? _request;
var client = createClient(true);
var client = createClient(allowBadCert: true);
// Attempt to open a connection to the server
try {
@ -511,8 +547,8 @@ class InvenTreeAPI {
// Set headers
_request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
_request.headers.set(HttpHeaders.acceptHeader, 'application/json');
_request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
_request.headers.set(HttpHeaders.acceptHeader, "application/json");
_request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
} on SocketException catch (error) {
@ -550,7 +586,7 @@ class InvenTreeAPI {
showServerError(L10().connectionRefused, error.toString());
} on TimeoutException {
showTimeoutError();
} catch (error, stackTrace) {
} catch (error) {
print("Error downloading image:");
print(error.toString());
showServerError(L10().downloadError, error.toString());
@ -561,7 +597,7 @@ class InvenTreeAPI {
* Upload a file to the given URL
*/
Future<APIResponse> uploadFile(String url, File f,
{String name = "attachment", String method="POST", Map<String, String>? fields}) async {
{String name = "attachment", String method="POST", Map<String, dynamic>? fields}) async {
var _url = makeApiUrl(url);
var request = http.MultipartRequest(method, Uri.parse(_url));
@ -569,8 +605,13 @@ class InvenTreeAPI {
request.headers.addAll(defaultHeaders());
if (fields != null) {
fields.forEach((String key, String value) {
request.fields[key] = value;
fields.forEach((String key, dynamic value) {
if (value == null) {
request.fields[key] = "";
} else {
request.fields[key] = value.toString();
}
});
}
@ -652,9 +693,9 @@ class InvenTreeAPI {
if (request == null) {
// Return an "invalid" APIResponse
return new APIResponse(
return APIResponse(
url: url,
method: 'OPTIONS'
method: "OPTIONS"
);
}
@ -671,9 +712,9 @@ class InvenTreeAPI {
if (request == null) {
// Return an "invalid" APIResponse
return new APIResponse(
return APIResponse(
url: url,
method: 'POST'
method: "POST"
);
}
@ -684,15 +725,13 @@ class InvenTreeAPI {
);
}
HttpClient createClient(bool allowBadCert) {
HttpClient createClient({bool allowBadCert = true}) {
var client = new HttpClient();
var client = HttpClient();
client.badCertificateCallback = ((X509Certificate cert, String host, int port) {
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
// TODO - Introspection of actual certificate?
allowBadCert = true;
if (allowBadCert) {
return true;
} else {
@ -702,7 +741,7 @@ class InvenTreeAPI {
);
return false;
}
});
};
// Set the connection timeout
client.connectionTimeout = Duration(seconds: 30);
@ -714,7 +753,7 @@ class InvenTreeAPI {
* Initiate a HTTP request to the server
*
* @param url is the API endpoint
* @param method is the HTTP method e.g. 'POST' / 'PATCH' / 'GET' etc;
* @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
* @param params is the request parameters
*/
Future<HttpClientRequest?> apiRequest(String url, String method, {Map<String, String> urlParams = const {}}) async {
@ -731,7 +770,7 @@ class InvenTreeAPI {
}
// Remove extraneous character if present
if (_url.endsWith('&')) {
if (_url.endsWith("&")) {
_url = _url.substring(0, _url.length - 1);
}
@ -749,7 +788,7 @@ class InvenTreeAPI {
HttpClientRequest? _request;
var client = createClient(true);
var client = createClient(allowBadCert: true);
// Attempt to open a connection to the server
try {
@ -757,8 +796,8 @@ class InvenTreeAPI {
// Set headers
_request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
_request.headers.set(HttpHeaders.acceptHeader, 'application/json');
_request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
_request.headers.set(HttpHeaders.acceptHeader, "application/json");
_request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
return _request;
@ -792,7 +831,7 @@ class InvenTreeAPI {
request.add(encoded_data);
}
APIResponse response = new APIResponse(
APIResponse response = APIResponse(
method: request.method,
url: request.uri.toString()
);
@ -805,6 +844,19 @@ class InvenTreeAPI {
// If the server returns a server error code, alert the user
if (_response.statusCode >= 500) {
showStatusCodeError(_response.statusCode);
sentryReportMessage(
"Server error",
context: {
"url": request.uri.toString(),
"method": request.method,
"statusCode": _response.statusCode.toString(),
"requestHeaders": request.headers.toString(),
"responseHeaders": _response.headers.toString(),
"responseData": response.data.toString(),
}
);
} else {
response.data = await responseToJson(_response) ?? {};
@ -814,21 +866,6 @@ class InvenTreeAPI {
if (statusCode != _response.statusCode) {
showStatusCodeError(_response.statusCode);
}
// Report any server errors
if (_response.statusCode >= 500) {
sentryReportMessage(
"Server error",
context: {
"url": request.uri.toString(),
"method": request.method,
"statusCode": _response.statusCode.toString(),
"requestHeaders": request.headers.toString(),
"responseHeaders": _response.headers.toString(),
"responseData": response.data.toString(),
}
);
}
}
}
@ -898,9 +935,9 @@ class InvenTreeAPI {
if (request == null) {
// Return an "invalid" APIResponse
return new APIResponse(
return APIResponse(
url: url,
method: 'GET',
method: "GET",
error: "HttpClientRequest is null",
);
}
@ -910,11 +947,11 @@ class InvenTreeAPI {
// Return a list of request headers
Map<String, String> defaultHeaders() {
var headers = Map<String, String>();
Map<String, String> headers = {};
headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
headers[HttpHeaders.acceptHeader] = 'application/json';
headers[HttpHeaders.contentTypeHeader] = 'application/json';
headers[HttpHeaders.acceptHeader] = "application/json";
headers[HttpHeaders.contentTypeHeader] = "application/json";
headers[HttpHeaders.acceptLanguageHeader] = Intl.getCurrentLocale();
return headers;
@ -924,7 +961,7 @@ class InvenTreeAPI {
if (_token.isNotEmpty) {
return "Token $_token";
} else if (profile != null) {
return "Basic " + base64Encode(utf8.encode('${profile?.username}:${profile?.password}'));
return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}"));
} else {
return "";
}
@ -954,10 +991,10 @@ class InvenTreeAPI {
)
);
return new CachedNetworkImage(
return CachedNetworkImage(
imageUrl: url,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(FontAwesomeIcons.exclamation),
errorWidget: (context, url, error) => FaIcon(FontAwesomeIcons.timesCircle, color: COLOR_DANGER),
httpHeaders: defaultHeaders(),
height: height,
width: width,

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import 'dart:ui';
import "dart:ui";
const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);

View File

@ -2,14 +2,20 @@
* Class for managing app-level configuration options
*/
import 'package:sembast/sembast.dart';
import 'package:inventree/preferences.dart';
import "package:sembast/sembast.dart";
import "package:inventree/preferences.dart";
class InvenTreeSettingsManager {
factory InvenTreeSettingsManager() {
return _manager;
}
InvenTreeSettingsManager._internal();
final store = StoreRef("settings");
Future<Database> get _db async => await InvenTreePreferencesDB.instance.database;
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
Future<dynamic> getValue(String key, dynamic backup) async {
@ -22,17 +28,22 @@ class InvenTreeSettingsManager {
return value;
}
// Load a boolean setting
Future<bool> getBool(String key, bool backup) async {
final dynamic value = await getValue(key, backup);
if (value is bool) {
return value;
} else {
return backup;
}
}
Future<void> setValue(String key, dynamic value) async {
await store.record(key).put(await _db, value);
}
// Ensure we only ever create a single instance of this class
static final InvenTreeSettingsManager _manager = new InvenTreeSettingsManager._internal();
factory InvenTreeSettingsManager() {
return _manager;
}
InvenTreeSettingsManager._internal();
}
static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal();
}

View File

@ -1,26 +1,24 @@
import 'package:inventree/app_settings.dart';
import 'package:inventree/inventree/sentry.dart';
import 'package:inventree/widget/dialogs.dart';
import 'package:inventree/widget/snacks.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:one_context/one_context.dart';
import "dart:io";
import 'package:qr_code_scanner/qr_code_scanner.dart';
import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/snacks.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:one_context/one_context.dart";
import 'package:inventree/inventree/stock.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/l10.dart';
import "package:qr_code_scanner/qr_code_scanner.dart";
import 'package:inventree/api.dart';
import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/l10.dart";
import "package:inventree/helpers.dart";
import "package:inventree/api.dart";
import 'package:inventree/widget/location_display.dart';
import 'package:inventree/widget/part_detail.dart';
import 'package:inventree/widget/stock_detail.dart';
import 'dart:io';
import "package:inventree/widget/location_display.dart";
import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/stock_detail.dart";
class BarcodeHandler {
@ -32,31 +30,11 @@ class BarcodeHandler {
* based on the response returned from the InvenTree server
*/
String getOverlayText(BuildContext context) => "Barcode Overlay";
BarcodeHandler();
BarcodeHandler();
String getOverlayText(BuildContext context) => "Barcode Overlay";
QRViewController? _controller;
void successTone() async {
final bool en = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
if (en) {
final player = AudioCache();
player.play("sounds/barcode_scan.mp3");
}
}
void failureTone() async {
final bool en = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
if (en) {
final player = AudioCache();
player.play("sounds/barcode_error.mp3");
}
}
QRViewController? _controller;
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// Called when the server "matches" a barcode
@ -101,8 +79,10 @@ class BarcodeHandler {
_controller?.resumeCamera();
Map<String, dynamic> data = response.asMap();
// Handle strange response from the server
if (!response.isValid() || response.data == null || !(response.data is Map)) {
if (!response.isValid() || !response.isMap()) {
onBarcodeUnknown(context, {});
// We want to know about this one!
@ -118,12 +98,12 @@ class BarcodeHandler {
"errorDetail": response.errorDetail,
}
);
} else if (response.data.containsKey('error')) {
onBarcodeUnknown(context, response.data);
} else if (response.data.containsKey('success')) {
onBarcodeMatched(context, response.data);
} else if (data.containsKey("error")) {
onBarcodeUnknown(context, data);
} else if (data.containsKey("success")) {
onBarcodeMatched(context, data);
} else {
onBarcodeUnhandled(context, response.data);
onBarcodeUnhandled(context, data);
}
}
}
@ -156,9 +136,9 @@ class BarcodeScanHandler extends BarcodeHandler {
int pk = -1;
// A stocklocation has been passed?
if (data.containsKey('stocklocation')) {
if (data.containsKey("stocklocation")) {
pk = (data['stocklocation']?['pk'] ?? -1) as int;
pk = (data["stocklocation"]?["pk"] ?? -1) as int;
if (pk > 0) {
@ -180,9 +160,9 @@ class BarcodeScanHandler extends BarcodeHandler {
);
}
} else if (data.containsKey('stockitem')) {
} else if (data.containsKey("stockitem")) {
pk = (data['stockitem']?['pk'] ?? -1) as int;
pk = (data["stockitem"]?["pk"] ?? -1) as int;
if (pk > 0) {
@ -206,9 +186,9 @@ class BarcodeScanHandler extends BarcodeHandler {
success: false
);
}
} else if (data.containsKey('part')) {
} else if (data.containsKey("part")) {
pk = (data['part']?['pk'] ?? -1) as int;
pk = (data["part"]?["pk"] ?? -1) as int;
if (pk > 0) {
@ -258,93 +238,24 @@ class BarcodeScanHandler extends BarcodeHandler {
}
}
class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
/*
* Barcode handler for assigning a new barcode to a stock item
*/
final InvenTreeStockItem item;
StockItemBarcodeAssignmentHandler(this.item);
@override
String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
@override
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
failureTone();
// If the barcode is known, we can't assign it to the stock item!
showSnackIcon(
L10().barcodeInUse,
icon: FontAwesomeIcons.qrcode,
success: false
);
}
@override
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
// If the barcode is unknown, we *can* assign it to the stock item!
if (!data.containsKey("hash")) {
showServerError(
L10().missingData,
L10().barcodeMissingHash,
);
} else {
// Send the 'hash' code as the UID for the stock item
item.update(
values: {
"uid": data['hash'],
}
).then((result) {
if (result) {
failureTone();
Navigator.of(context).pop();
showSnackIcon(
L10().barcodeAssigned,
success: true,
icon: FontAwesomeIcons.qrcode
);
} else {
successTone();
showSnackIcon(
L10().barcodeNotAssigned,
success: false,
icon: FontAwesomeIcons.qrcode
);
}
});
}
}
}
class StockItemScanIntoLocationHandler extends BarcodeHandler {
/*
* Barcode handler for scanning a provided StockItem into a scanned StockLocation
*/
final InvenTreeStockItem item;
StockItemScanIntoLocationHandler(this.item);
final InvenTreeStockItem item;
@override
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
@override
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// If the barcode points to a 'stocklocation', great!
if (data.containsKey('stocklocation')) {
// If the barcode points to a "stocklocation", great!
if (data.containsKey("stocklocation")) {
// Extract location information
int location = (data['stocklocation']['pk'] ?? -1) as int;
int location = (data["stocklocation"]["pk"] ?? -1) as int;
if (location == -1) {
showSnackIcon(
@ -394,11 +305,11 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
/*
* Barcode handler for scanning stock item(s) into the specified StockLocation
*/
final InvenTreeStockLocation location;
StockLocationScanInItemsHandler(this.location);
final InvenTreeStockLocation location;
@override
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
@ -406,11 +317,11 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// Returned barcode must match a stock item
if (data.containsKey('stockitem')) {
if (data.containsKey("stockitem")) {
int item_id = data['stockitem']['pk'] as int;
int item_id = data["stockitem"]["pk"] as int;
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem;
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem?;
if (item == null) {
@ -462,11 +373,78 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
}
class UniqueBarcodeHandler extends BarcodeHandler {
/*
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
*/
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
// Callback function when a "unique" barcode hash is found
final Function(String) callback;
final String overlayText;
@override
String getOverlayText(BuildContext context) {
if (overlayText.isEmpty) {
return L10().barcodeScanAssign;
} else {
return overlayText;
}
}
@override
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
failureTone();
// If the barcode is known, we can"t assign it to the stock item!
showSnackIcon(
L10().barcodeInUse,
icon: FontAwesomeIcons.qrcode,
success: false
);
}
@override
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
// If the barcode is unknown, we *can* assign it to the stock item!
if (!data.containsKey("hash")) {
showServerError(
L10().missingData,
L10().barcodeMissingHash,
);
} else {
String hash = (data["hash"] ?? "") as String;
if (hash.isEmpty) {
failureTone();
showSnackIcon(
L10().barcodeError,
success: false,
);
} else {
successTone();
// Close the barcode scanner
Navigator.of(context).pop();
callback(hash);
}
}
}
}
class InvenTreeQRView extends StatefulWidget {
final BarcodeHandler _handler;
const InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
final BarcodeHandler _handler;
@override
State<StatefulWidget> createState() => _QRViewState(_handler);
@ -475,7 +453,9 @@ class InvenTreeQRView extends StatefulWidget {
class _QRViewState extends State<InvenTreeQRView> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
_QRViewState(this._handler) : super();
final GlobalKey qrKey = GlobalKey(debugLabel: "QR");
QRViewController? _controller;
@ -494,8 +474,6 @@ class _QRViewState extends State<InvenTreeQRView> {
_controller!.resumeCamera();
}
_QRViewState(this._handler) : super();
void _onViewCreated(BuildContext context, QRViewController controller) {
_controller = controller;
controller.scannedDataStream.listen((barcode) {

3
lib/dummy_dsn.dart Normal file
View File

@ -0,0 +1,3 @@
// Dummy DSN to use for unit testing, etc
const String SENTRY_DSN_KEY = "https://12345678901234567890@abcdef.ingest.sentry.io/11223344";

View File

@ -12,11 +12,9 @@ import 'package:flutter/material.dart';
class S implements WidgetsLocalizations {
const S();
static const GeneratedLocalizationsDelegate delegate =
const GeneratedLocalizationsDelegate();
static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate();
static S of(BuildContext context) =>
Localizations.of<S>(context, WidgetsLocalizations);
static S of(BuildContext context) => Localizations.of<S>(context, WidgetsLocalizations);
@override
TextDirection get textDirection => TextDirection.ltr;

37
lib/helpers.dart Normal file
View File

@ -0,0 +1,37 @@
/*
* A set of helper functions to reduce boilerplate code
*/
/*
* Simplify a numerical value into a string,
* supressing trailing zeroes
*/
import "package:audioplayers/audioplayers.dart";
import "package:inventree/app_settings.dart";
String simpleNumberString(double number) {
// Ref: https://stackoverflow.com/questions/55152175/how-to-remove-trailing-zeros-using-dart
return number.toStringAsFixed(number.truncateToDouble() == number ? 0 : 1);
}
Future<void> successTone() async {
final bool en = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
if (en) {
final player = AudioCache();
player.play("sounds/barcode_scan.mp3");
}
}
Future <void> failureTone() async {
final bool en = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
if (en) {
final player = AudioCache();
player.play("sounds/barcode_error.mp3");
}
}

View File

@ -1,6 +1,8 @@
import 'package:inventree/api.dart';
import "dart:async";
import 'model.dart';
import "package:inventree/api.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/purchase_order.dart";
/*
@ -9,6 +11,10 @@ import 'model.dart';
class InvenTreeCompany extends InvenTreeModel {
InvenTreeCompany() : super();
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "company/";
@ -25,25 +31,51 @@ class InvenTreeCompany extends InvenTreeModel {
};
}
InvenTreeCompany() : super();
String get image => (jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage) as String;
String get image => jsondata['image'] ?? jsondata['thumbnail'] ?? InvenTreeAPI.staticImage;
String get thumbnail => (jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb) as String;
String get thumbnail => jsondata['thumbnail'] ?? jsondata['image'] ?? InvenTreeAPI.staticThumb;
String get website => (jsondata["website"] ?? "") as String;
String get website => jsondata['website'] ?? '';
String get phone => (jsondata["phone"] ?? "") as String;
String get phone => jsondata['phone'] ?? '';
String get email => (jsondata["email"] ?? "") as String;
String get email => jsondata['email'] ?? '';
bool get isSupplier => (jsondata["is_supplier"] ?? false) as bool;
bool get isSupplier => jsondata['is_supplier'] ?? false;
bool get isManufacturer => (jsondata["is_manufacturer"] ?? false) as bool;
bool get isManufacturer => jsondata['is_manufacturer'] ?? false;
bool get isCustomer => (jsondata["is_customer"] ?? false) as bool;
bool get isCustomer => jsondata['is_customer'] ?? false;
int get partSuppliedCount => (jsondata["parts_supplied"] ?? 0) as int;
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
int get partManufacturedCount => (jsondata["parts_manufactured"] ?? 0) as int;
// Request a list of purchase orders against this company
Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({bool? outstanding}) async {
Map<String, String> filters = {
"supplier": "${pk}"
};
if (outstanding != null) {
filters["outstanding"] = outstanding ? "true" : "false";
}
final List<InvenTreeModel> results = await InvenTreePurchaseOrder().list(
filters: filters
);
List<InvenTreePurchaseOrder> orders = [];
for (InvenTreeModel model in results) {
if (model is InvenTreePurchaseOrder) {
orders.add(model);
}
}
return orders;
}
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -58,6 +90,11 @@ class InvenTreeCompany extends InvenTreeModel {
* The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
*/
class InvenTreeSupplierPart extends InvenTreeModel {
InvenTreeSupplierPart() : super();
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "company/part/";
@ -79,27 +116,29 @@ class InvenTreeSupplierPart extends InvenTreeModel {
return _filters();
}
InvenTreeSupplierPart() : super();
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
String get manufacturerName => (jsondata["manufacturer_detail"]["name"] ?? "") as String;
int get manufacturerId => (jsondata['manufacturer'] ?? -1) as int;
String get manufacturerImage => (jsondata["manufacturer_detail"]["image"] ?? jsondata["manufacturer_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get manufacturerName => jsondata['manufacturer_detail']['name'];
int get manufacturerPartId => (jsondata["manufacturer_part"] ?? -1) as int;
String get manufacturerImage => jsondata['manufacturer_detail']['image'] ?? jsondata['manufacturer_detail']['thumbnail'];
int get supplierId => (jsondata["supplier"] ?? -1) as int;
int get manufacturerPartId => (jsondata['manufacturer_part'] ?? -1) as int;
String get supplierName => (jsondata["supplier_detail"]["name"] ?? "") as String;
int get supplierId => (jsondata['supplier'] ?? -1) as int;
String get supplierImage => (jsondata["supplier_detail"]["image"] ?? jsondata["supplier_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get supplierName => jsondata['supplier_detail']['name'];
String get SKU => (jsondata["SKU"] ?? "") as String;
String get supplierImage => jsondata['supplier_detail']['image'] ?? jsondata['supplier_detail']['thumbnail'];
String get MPN => (jsondata["MPN"] ?? "") as String;
String get SKU => (jsondata['SKU'] ?? '') as String;
int get partId => (jsondata["part"] ?? -1) as int;
String get MPN => jsondata['MPN'] ?? '';
String get partImage => (jsondata["part_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get partName => (jsondata["part_detail"]["full_name"] ?? "") as String;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -112,6 +151,10 @@ class InvenTreeSupplierPart extends InvenTreeModel {
class InvenTreeManufacturerPart extends InvenTreeModel {
InvenTreeManufacturerPart() : super();
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String url = "company/part/manufacturer/";
@ -122,15 +165,11 @@ class InvenTreeManufacturerPart extends InvenTreeModel {
};
}
InvenTreeManufacturerPart() : super();
int get partId => (jsondata["part"] ?? -1) as int;
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
int get partId => (jsondata['part'] ?? -1) as int;
int get manufacturerId => (jsondata['manufacturer'] ?? -1) as int;
String get MPN => (jsondata['MPN'] ?? '') as String;
String get MPN => (jsondata["MPN"] ?? "") as String;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {

View File

@ -1,18 +1,17 @@
import 'dart:async';
import 'dart:io';
import "dart:async";
import "dart:io";
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/api.dart';
import 'package:flutter/cupertino.dart';
import 'package:inventree/inventree/sentry.dart';
import 'package:inventree/widget/dialogs.dart';
import 'package:url_launcher/url_launcher.dart';
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart";
import "package:flutter/cupertino.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart";
import "package:url_launcher/url_launcher.dart";
import 'package:path/path.dart' as path;
import 'package:http/http.dart' as http;
import "package:path/path.dart" as path;
import '../l10.dart';
import '../api_form.dart';
import "package:inventree/l10.dart";
import "package:inventree/api_form.dart";
// Paginated response object
@ -40,12 +39,17 @@ class InvenTreePageResponse {
*/
class InvenTreeModel {
InvenTreeModel();
// Construct an InvenTreeModel from a JSON data object
InvenTreeModel.fromJson(this.jsondata);
// Override the endpoint URL for each subclass
String get URL => "";
// Override the web URL for each subclass
// Note: If the WEB_URL is the same (except for /api/) as URL then just leave blank
String WEB_URL = "";
String get WEB_URL => "";
String get webUrl {
@ -114,36 +118,23 @@ class InvenTreeModel {
Map<String, dynamic> jsondata = {};
// Accessor for the API
var api = InvenTreeAPI();
InvenTreeAPI get api => InvenTreeAPI();
// Default empty object constructor
InvenTreeModel() {
jsondata.clear();
}
// Construct an InvenTreeModel from a JSON data object
InvenTreeModel.fromJson(Map<String, dynamic> json) {
// Store the json object
jsondata = json;
}
int get pk => (jsondata['pk'] ?? -1) as int;
int get pk => (jsondata["pk"] ?? -1) as int;
// Some common accessors
String get name => jsondata['name'] ?? '';
String get name => (jsondata["name"] ?? "") as String;
String get description => jsondata['description'] ?? '';
String get description => (jsondata["description"] ?? "") as String;
String get notes => jsondata['notes'] ?? '';
String get notes => (jsondata["notes"] ?? "") as String;
int get parentId => (jsondata['parent'] ?? -1) as int;
int get parentId => (jsondata["parent"] ?? -1) as int;
// Legacy API provided external link as "URL", while newer API uses "link"
String get link => jsondata['link'] ?? jsondata['URL'] ?? '';
String get link => (jsondata["link"] ?? jsondata["URL"] ?? "") as String;
void goToInvenTreePage() async {
Future <void> goToInvenTreePage() async {
if (await canLaunch(webUrl)) {
await launch(webUrl);
@ -152,7 +143,7 @@ class InvenTreeModel {
}
}
void openLink() async {
Future <void> openLink() async {
if (link.isNotEmpty) {
@ -162,7 +153,7 @@ class InvenTreeModel {
}
}
String get keywords => jsondata['keywords'] ?? '';
String get keywords => (jsondata["keywords"] ?? "") as String;
// Create a new object from JSON data (not a constructor!)
InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -176,20 +167,60 @@ class InvenTreeModel {
String get url => "${URL}/${pk}/".replaceAll("//", "/");
// 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(String searchTerm, {Map<String, String> filters = const {}, int offset = 0, int limit = 25}) async {
filters["search"] = searchTerm;
Map<String, String> searchFilters = {};
final results = list(filters: filters);
for (String key in filters.keys) {
searchFilters[key] = filters[key] ?? "";
}
searchFilters["search"] = searchTerm;
searchFilters["offset"] = "${offset}";
searchFilters["limit"] = "${limit}";
final results = list(filters: searchFilters);
return results;
}
Map<String, String> defaultListFilters() { return Map<String, String>(); }
// Return the number of results that would meet a particular "query"
Future<int> count({Map<String, String> filters = const {}, String searchQuery = ""} ) async {
var params = defaultListFilters();
filters.forEach((String key, String value) {
params[key] = value;
});
if (searchQuery.isNotEmpty) {
params["search"] = searchQuery;
}
// Limit to 1 result, for quick DB access
params["limit"] = "1";
var response = await api.get(URL, params: params);
if (response.isValid()) {
int n = int.tryParse(response.data["count"].toString()) ?? 0;
print("${URL} -> ${n} results");
return n;
} else {
return 0;
}
}
Map<String, String> defaultListFilters() {
return {};
}
// A map of "default" headers to use when performing a GET request
Map<String, String> defaultGetFilters() { return Map<String, String>(); }
Map<String, String> defaultGetFilters() {
return {};
}
/*
* Reload this object, by requesting data from the server
@ -198,7 +229,7 @@ class InvenTreeModel {
var response = await api.get(url, params: defaultGetFilters(), expectedStatusCode: 200);
if (!response.isValid() || response.data == null || !(response.data is Map)) {
if (!response.isValid() || response.data == null || (response.data is! Map)) {
// Report error
if (response.statusCode > 0) {
@ -224,7 +255,7 @@ class InvenTreeModel {
}
jsondata = response.data;
jsondata = response.asMap();
return true;
}
@ -267,12 +298,12 @@ class InvenTreeModel {
// Override any default values
for (String key in filters.keys) {
params[key] = filters[key] ?? '';
params[key] = filters[key] ?? "";
}
var response = await api.get(url, params: params);
if (!response.isValid() || response.data == null || !(response.data is Map)) {
if (!response.isValid() || response.data == null || response.data is! Map) {
if (response.statusCode > 0) {
await sentryReportMessage(
@ -297,25 +328,23 @@ class InvenTreeModel {
}
return createFromJson(response.data);
return createFromJson(response.asMap());
}
Future<InvenTreeModel?> create(Map<String, dynamic> data) async {
print("CREATE: ${URL} ${data.toString()}");
if (data.containsKey('pk')) {
data.remove('pk');
if (data.containsKey("pk")) {
data.remove("pk");
}
if (data.containsKey('id')) {
data.remove('id');
if (data.containsKey("id")) {
data.remove("id");
}
var response = await api.post(URL, body: data);
// Invalid response returned from server
if (!response.isValid() || response.data == null || !(response.data is Map)) {
if (!response.isValid() || response.data == null || response.data is! Map) {
if (response.statusCode > 0) {
await sentryReportMessage(
@ -340,19 +369,34 @@ class InvenTreeModel {
return null;
}
return createFromJson(response.data);
return createFromJson(response.asMap());
}
Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async {
var params = defaultListFilters();
for (String key in filters.keys) {
params[key] = filters[key] ?? '';
params[key] = filters[key] ?? "";
}
params["limit"] = "${limit}";
params["offset"] = "${offset}";
/* Special case: "original_search":
* - We may wish to provide an original "query" which is augmented by the user
* - Thus, "search" and "original_search" may both be provided
* - In such a case, we want to concatenate them together
*/
if (params.containsKey("original_search")) {
String search = params["search"] ?? "";
String original = params["original_search"] ?? "";
params["search"] = "${search} ${original}".trim();
params.remove("original_search");
}
var response = await api.get(URL, params: params);
if (!response.isValid()) {
@ -360,15 +404,17 @@ class InvenTreeModel {
}
// Construct the response
InvenTreePageResponse page = new InvenTreePageResponse();
InvenTreePageResponse page = InvenTreePageResponse();
if (response.data.containsKey("count") && response.data.containsKey("results")) {
page.count = response.data["count"] as int;
var data = response.asMap();
if (data.containsKey("count") && data.containsKey("results")) {
page.count = (data["count"] ?? 0) as int;
page.results = [];
for (var result in response.data["results"]) {
page.addResult(createFromJson(result));
page.addResult(createFromJson(result as Map<String, dynamic>));
}
return page;
@ -384,7 +430,7 @@ class InvenTreeModel {
var params = defaultListFilters();
for (String key in filters.keys) {
params[key] = filters[key] ?? '';
params[key] = filters[key] ?? "";
}
var response = await api.get(URL, params: params);
@ -396,20 +442,22 @@ class InvenTreeModel {
return results;
}
dynamic data;
List<dynamic> data = [];
if (response.data is List) {
data = response.data;
} else if (response.data.containsKey('results')) {
data = response.data['results'];
} else {
data = [];
if (response.isList()) {
data = response.asList();
} else if (response.isMap()) {
var mData = response.asMap();
if (mData.containsKey("results")) {
data = (response.data["results"] ?? []) as List<dynamic>;
}
}
for (var d in data) {
// Create a new object (of the current class type
InvenTreeModel obj = createFromJson(d);
InvenTreeModel obj = createFromJson(d as Map<String, dynamic>);
results.add(obj);
}
@ -421,9 +469,9 @@ class InvenTreeModel {
// Provide a listing of objects at the endpoint
// TODO - Static function which returns a list of objects (of this class)
// TODO - Define a 'delete' function
// TODO - Define a "delete" function
// TODO - Define a 'save' / 'update' function
// TODO - Define a "save" / "update" function
// Override this function for each sub-class
bool matchAgainstString(String filter) {
@ -457,10 +505,11 @@ class InvenTreeModel {
class InvenTreeAttachment extends InvenTreeModel {
// Class representing an "attachment" file
InvenTreeAttachment() : super();
String get attachment => jsondata["attachment"] ?? '';
InvenTreeAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
String get attachment => (jsondata["attachment"] ?? "") as String;
// Return the filename of the attachment
String get filename {
@ -498,25 +547,23 @@ class InvenTreeAttachment extends InvenTreeModel {
return FontAwesomeIcons.fileAlt;
}
String get comment => jsondata["comment"] ?? '';
String get comment => (jsondata["comment"] ?? "") as String;
DateTime? get uploadDate {
if (jsondata.containsKey("upload_date")) {
return DateTime.tryParse(jsondata["upload_date"] ?? '');
return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
} else {
return null;
}
}
InvenTreeAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async {
final APIResponse response = await InvenTreeAPI().uploadFile(
URL,
attachment,
method: 'POST',
name: 'attachment',
method: "POST",
name: "attachment",
fields: fields
);

View File

@ -1,15 +1,19 @@
import 'package:inventree/api.dart';
import 'package:inventree/inventree/stock.dart';
import 'package:inventree/inventree/company.dart';
import 'package:flutter/cupertino.dart';
import 'package:inventree/l10.dart';
import "dart:io";
import 'model.dart';
import 'dart:io';
import 'package:http/http.dart' as http;
import "package:inventree/api.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/company.dart";
import "package:flutter/cupertino.dart";
import "package:inventree/l10.dart";
import "model.dart";
class InvenTreePartCategory extends InvenTreeModel {
InvenTreePartCategory() : super();
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "part/category/";
@ -25,21 +29,20 @@ class InvenTreePartCategory extends InvenTreeModel {
@override
Map<String, String> defaultListFilters() {
var filters = new Map<String, String>();
filters["active"] = "true";
filters["cascade"] = "false";
return filters;
return {
"active": "true",
"cascade": "false"
};
}
String get pathstring => jsondata['pathstring'] ?? '';
String get pathstring => (jsondata["pathstring"] ?? "") as String;
String get parentpathstring {
// TODO - Drive the refactor tractor through this
List<String> psplit = pathstring.split("/");
if (psplit.length > 0) {
if (psplit.isNotEmpty) {
psplit.removeLast();
}
@ -52,11 +55,7 @@ class InvenTreePartCategory extends InvenTreeModel {
return p;
}
int get partcount => jsondata['parts'] ?? 0;
InvenTreePartCategory() : super();
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
int get partcount => (jsondata["parts"] ?? 0) as int;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -71,25 +70,23 @@ class InvenTreePartCategory extends InvenTreeModel {
class InvenTreePartTestTemplate extends InvenTreeModel {
@override
String get URL => "part/test-template/";
String get key => jsondata['key'] ?? '';
String get testName => jsondata['test_name'] ?? '';
String get description => jsondata['description'] ?? '';
bool get required => jsondata['required'] ?? false;
bool get requiresValue => jsondata['requires_value'] ?? false;
bool get requiresAttachment => jsondata['requires_attachment'] ?? false;
InvenTreePartTestTemplate() : super();
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "part/test-template/";
String get key => (jsondata["key"] ?? "") as String;
String get testName => (jsondata["test_name"] ?? "") as String;
bool get required => (jsondata["required"] ?? false) as bool;
bool get requiresValue => (jsondata["requires_value"] ?? false) as bool;
bool get requiresAttachment => (jsondata["requires_attachment"] ?? false) as bool;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
var template = InvenTreePartTestTemplate.fromJson(json);
@ -125,6 +122,10 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
class InvenTreePart extends InvenTreeModel {
InvenTreePart() : super();
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "part/";
@ -138,9 +139,9 @@ class InvenTreePart extends InvenTreeModel {
"keywords": {},
"link": {},
// Parent category
"category": {
},
"category": {},
"default_location": {},
"units": {},
@ -195,7 +196,7 @@ class InvenTreePart extends InvenTreeModel {
});
}
int get supplierCount => (jsondata['suppliers'] ?? 0) as int;
int get supplierCount => (jsondata["suppliers"] ?? 0) as int;
// Request supplier parts for this part
Future<List<InvenTreeSupplierPart>> getSupplierParts() async {
@ -241,8 +242,10 @@ class InvenTreePart extends InvenTreeModel {
});
}
int? get defaultLocation => jsondata["default_location"] as int?;
// Get the number of stock on order for this Part
double get onOrder => double.tryParse(jsondata['ordering'].toString()) ?? 0;
double get onOrder => double.tryParse(jsondata["ordering"].toString()) ?? 0;
String get onOrderString {
@ -254,7 +257,7 @@ class InvenTreePart extends InvenTreeModel {
}
// Get the stock count for this Part
double get inStock => double.tryParse(jsondata['in_stock'].toString()) ?? 0;
double get inStock => double.tryParse(jsondata["in_stock"].toString()) ?? 0;
String get inStockString {
@ -271,69 +274,69 @@ class InvenTreePart extends InvenTreeModel {
return q;
}
String get units => jsondata["units"] ?? "";
String get units => (jsondata["units"] ?? "") as String;
// Get the number of units being build for this Part
double get building => double.tryParse(jsondata['building'].toString()) ?? 0;
double get building => double.tryParse(jsondata["building"].toString()) ?? 0;
// Get the number of BOM items in this Part (if it is an assembly)
int get bomItemCount => (jsondata['bom_items'] ?? 0) as int;
int get bomItemCount => (jsondata["bom_items"] ?? 0) as int;
// Get the number of BOMs this Part is used in (if it is a component)
int get usedInCount => (jsondata['used_in'] ?? 0) as int;
int get usedInCount => (jsondata["used_in"] ?? 0) as int;
bool get isAssembly => (jsondata['assembly'] ?? false) as bool;
bool get isAssembly => (jsondata["assembly"] ?? false) as bool;
bool get isComponent => (jsondata['component'] ?? false) as bool;
bool get isComponent => (jsondata["component"] ?? false) as bool;
bool get isPurchaseable => (jsondata['purchaseable'] ?? false) as bool;
bool get isPurchaseable => (jsondata["purchaseable"] ?? false) as bool;
bool get isSalable => (jsondata['salable'] ?? false) as bool;
bool get isSalable => (jsondata["salable"] ?? false) as bool;
bool get isActive => (jsondata['active'] ?? false) as bool;
bool get isActive => (jsondata["active"] ?? false) as bool;
bool get isVirtual => (jsondata['virtual'] ?? false) as bool;
bool get isVirtual => (jsondata["virtual"] ?? false) as bool;
bool get isTrackable => (jsondata['trackable'] ?? false) as bool;
bool get isTrackable => (jsondata["trackable"] ?? false) as bool;
// Get the IPN (internal part number) for the Part instance
String get IPN => jsondata['IPN'] ?? '';
String get IPN => (jsondata["IPN"] ?? "") as String;
// Get the revision string for the Part instance
String get revision => jsondata['revision'] ?? '';
String get revision => (jsondata["revision"] ?? "") as String;
// Get the category ID for the Part instance (or 'null' if does not exist)
int get categoryId => (jsondata['category'] ?? -1) as int;
// Get the category ID for the Part instance (or "null" if does not exist)
int get categoryId => (jsondata["category"] ?? -1) as int;
// Get the category name for the Part instance
String get categoryName {
// Inavlid category ID
if (categoryId <= 0) return '';
if (categoryId <= 0) return "";
if (!jsondata.containsKey('category_detail')) return '';
if (!jsondata.containsKey("category_detail")) return "";
return jsondata['category_detail']?['name'] ?? '';
return (jsondata["category_detail"]?["name"] ?? "") as String;
}
// Get the category description for the Part instance
String get categoryDescription {
// Invalid category ID
if (categoryId <= 0) return '';
if (categoryId <= 0) return "";
if (!jsondata.containsKey('category_detail')) return '';
if (!jsondata.containsKey("category_detail")) return "";
return jsondata['category_detail']?['description'] ?? '';
return (jsondata["category_detail"]?["description"] ?? "") as String;
}
// Get the image URL for the Part instance
String get _image => jsondata['image'] ?? '';
String get _image => (jsondata["image"] ?? "") as String;
// Get the thumbnail URL for the Part instance
String get _thumbnail => jsondata['thumbnail'] ?? '';
String get _thumbnail => (jsondata["thumbnail"] ?? "") as String;
// Return the fully-qualified name for the Part instance
String get fullname {
String fn = jsondata['full_name'] ?? '';
String fn = (jsondata["full_name"] ?? "") as String;
if (fn.isNotEmpty) return fn;
@ -369,21 +372,15 @@ class InvenTreePart extends InvenTreeModel {
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
image,
method: 'PATCH',
name: 'image',
method: "PATCH",
name: "image",
);
return response.successful();
}
// Return the "starred" status of this part
bool get starred => (jsondata['starred'] ?? false) as bool;
InvenTreePart() : super();
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
// TODO
}
bool get starred => (jsondata["starred"] ?? false) as bool;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -399,11 +396,11 @@ class InvenTreePartAttachment extends InvenTreeAttachment {
InvenTreePartAttachment() : super();
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "part/attachment/";
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
return InvenTreePartAttachment.fromJson(json);

View File

@ -0,0 +1,205 @@
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/model.dart";
// TODO: In the future, status codes should be retrieved from the server
const int PO_STATUS_PENDING = 10;
const int PO_STATUS_PLACED = 20;
const int PO_STATUS_COMPLETE = 30;
const int PO_STATUS_CANCELLED = 40;
const int PO_STATUS_LOST = 50;
const int PO_STATUS_RETURNED = 60;
class InvenTreePurchaseOrder extends InvenTreeModel {
InvenTreePurchaseOrder() : super();
InvenTreePurchaseOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "order/po/";
String get receive_url => "${url}receive/";
@override
Map<String, dynamic> formFields() {
return {
"reference": {},
"supplier_reference": {},
"description": {},
"target_date": {},
"link": {},
"responsible": {},
};
}
@override
Map<String, String> defaultGetFilters() {
return {
"supplier_detail": "true",
};
}
@override
Map<String, String> defaultListFilters() {
return {
"supplier_detail": "true",
};
}
String get issueDate => (jsondata["issue_date"] ?? "") as String;
String get completeDate => (jsondata["complete_date"] ?? "") as String;
String get creationDate => (jsondata["creation_date"] ?? "") as String;
String get targetDate => (jsondata["target_date"] ?? "") as String;
int get lineItemCount => (jsondata["line_items"] ?? 0) as int;
bool get overdue => (jsondata["overdue"] ?? false) as bool;
String get reference => (jsondata["reference"] ?? "") as String;
int get responsibleId => (jsondata["responsible"] ?? -1) as int;
int get supplierId => (jsondata["supplier"] ?? -1) as int;
InvenTreeCompany? get supplier {
dynamic supplier_detail = jsondata["supplier_detail"];
if (supplier_detail == null) {
return null;
} else {
return InvenTreeCompany.fromJson(supplier_detail as Map<String, dynamic>);
}
}
String get supplierReference => (jsondata["supplier_reference"] ?? "") as String;
int get status => (jsondata["status"] ?? -1) as int;
String get statusText => (jsondata["status_text"] ?? "") as String;
bool get isOpen => status == PO_STATUS_PENDING || status == PO_STATUS_PLACED;
bool get isPlaced => status == PO_STATUS_PLACED;
bool get isFailed => status == PO_STATUS_CANCELLED || status == PO_STATUS_LOST || status == PO_STATUS_RETURNED;
Future<List<InvenTreePOLineItem>> getLineItems() async {
final results = await InvenTreePOLineItem().list(
filters: {
"order": "${pk}",
}
);
List<InvenTreePOLineItem> items = [];
for (var result in results) {
if (result is InvenTreePOLineItem) {
items.add(result);
}
}
return items;
}
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
return InvenTreePurchaseOrder.fromJson(json);
}
}
class InvenTreePOLineItem extends InvenTreeModel {
InvenTreePOLineItem() : super();
InvenTreePOLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "order/po-line/";
@override
Map<String, dynamic> formFields() {
return {
// TODO: @Guusggg Not sure what will come here.
// "quantity": {},
// "reference": {},
// "notes": {},
// "order": {},
// "part": {},
"received": {},
// "purchase_price": {},
// "purchase_price_currency": {},
// "destination": {}
};
}
@override
Map<String, String> defaultGetFilters() {
return {
"part_detail": "true",
};
}
@override
Map<String, String> defaultListFilters() {
return {
"part_detail": "true",
};
}
bool get isComplete => received >= quantity;
double get quantity => (jsondata["quantity"] ?? 0) as double;
double get received => (jsondata["received"] ?? 0) as double;
double get outstanding => quantity - received;
String get reference => (jsondata["reference"] ?? "") as String;
int get orderId => (jsondata["order"] ?? -1) as int;
int get supplierPartId => (jsondata["part"] ?? -1) as int;
InvenTreePart? get part {
dynamic part_detail = jsondata["part_detail"];
if (part_detail == null) {
return null;
} else {
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
}
}
InvenTreeSupplierPart? get supplierPart {
dynamic detail = jsondata["supplier_part_detail"];
if (detail == null) {
return null;
} else {
return InvenTreeSupplierPart.fromJson(detail as Map<String, dynamic>);
}
}
double get purchasePrice => double.parse((jsondata["purchase_price"] ?? "") as String);
String get purchasePriceCurrency => (jsondata["purchase_price_currency"] ?? "") as String;
String get purchasePriceString => (jsondata["purchase_price_string"] ?? "") as String;
int get destination => (jsondata["destination"] ?? -1) as int;
Map<String, dynamic> get destinationDetail => (jsondata["destination_detail"] ?? {}) as Map<String, dynamic>;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {
return InvenTreePOLineItem.fromJson(json);
}
}

View File

@ -1,10 +1,10 @@
import 'dart:io';
import "dart:io";
import 'package:device_info_plus/device_info_plus.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import "package:device_info_plus/device_info_plus.dart";
import "package:package_info_plus/package_info_plus.dart";
import "package:sentry_flutter/sentry_flutter.dart";
import 'package:inventree/api.dart';
import "package:inventree/api.dart";
Future<Map<String, dynamic>> getDeviceInfo() async {
@ -18,35 +18,35 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
final iosDeviceInfo = await deviceInfo.iosInfo;
device_info = {
'name': iosDeviceInfo.name,
'model': iosDeviceInfo.model,
'systemName': iosDeviceInfo.systemName,
'systemVersion': iosDeviceInfo.systemVersion,
'localizedModel': iosDeviceInfo.localizedModel,
'utsname': iosDeviceInfo.utsname.sysname,
'identifierForVendor': iosDeviceInfo.identifierForVendor,
'isPhysicalDevice': iosDeviceInfo.isPhysicalDevice,
"name": iosDeviceInfo.name,
"model": iosDeviceInfo.model,
"systemName": iosDeviceInfo.systemName,
"systemVersion": iosDeviceInfo.systemVersion,
"localizedModel": iosDeviceInfo.localizedModel,
"utsname": iosDeviceInfo.utsname.sysname,
"identifierForVendor": iosDeviceInfo.identifierForVendor,
"isPhysicalDevice": iosDeviceInfo.isPhysicalDevice,
};
} else if (Platform.isAndroid) {
final androidDeviceInfo = await deviceInfo.androidInfo;
device_info = {
'type': androidDeviceInfo.type,
'model': androidDeviceInfo.model,
'device': androidDeviceInfo.device,
'id': androidDeviceInfo.id,
'androidId': androidDeviceInfo.androidId,
'brand': androidDeviceInfo.brand,
'display': androidDeviceInfo.display,
'hardware': androidDeviceInfo.hardware,
'manufacturer': androidDeviceInfo.manufacturer,
'product': androidDeviceInfo.product,
'version': androidDeviceInfo.version.release,
'supported32BitAbis': androidDeviceInfo.supported32BitAbis,
'supported64BitAbis': androidDeviceInfo.supported64BitAbis,
'supportedAbis': androidDeviceInfo.supportedAbis,
'isPhysicalDevice': androidDeviceInfo.isPhysicalDevice,
"type": androidDeviceInfo.type,
"model": androidDeviceInfo.model,
"device": androidDeviceInfo.device,
"id": androidDeviceInfo.id,
"androidId": androidDeviceInfo.androidId,
"brand": androidDeviceInfo.brand,
"display": androidDeviceInfo.display,
"hardware": androidDeviceInfo.hardware,
"manufacturer": androidDeviceInfo.manufacturer,
"product": androidDeviceInfo.product,
"version": androidDeviceInfo.version.release,
"supported32BitAbis": androidDeviceInfo.supported32BitAbis,
"supported64BitAbis": androidDeviceInfo.supported64BitAbis,
"supportedAbis": androidDeviceInfo.supportedAbis,
"isPhysicalDevice": androidDeviceInfo.isPhysicalDevice,
};
}
@ -90,7 +90,7 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
if (isInDebugMode()) {
print('----- In dev mode. Not sending message to Sentry.io -----');
print("----- In dev mode. Not sending message to Sentry.io -----");
return true;
}
@ -117,7 +117,7 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
print('----- Sentry Intercepted error: $error -----');
print("----- Sentry Intercepted error: $error -----");
print(stackTrace);
// Errors thrown in development mode are unlikely to be interesting. You can
@ -125,7 +125,7 @@ Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
// the report.
if (isInDebugMode()) {
print('----- In dev mode. Not sending report to Sentry.io -----');
print("----- In dev mode. Not sending report to Sentry.io -----");
return;
}

View File

@ -1,19 +1,23 @@
import 'package:intl/intl.dart';
import 'package:inventree/inventree/part.dart';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'model.dart';
import 'package:inventree/l10.dart';
import "dart:async";
import "package:flutter/material.dart";
import "package:intl/intl.dart";
import "package:inventree/helpers.dart";
import "package:inventree/inventree/part.dart";
import "package:flutter/cupertino.dart";
import 'dart:async';
import 'dart:io';
import "package:inventree/inventree/model.dart";
import "package:inventree/l10.dart";
import 'package:inventree/api.dart';
import "package:inventree/api.dart";
class InvenTreeStockItemTestResult extends InvenTreeModel {
InvenTreeStockItemTestResult() : super();
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "stock/test/";
@ -31,23 +35,17 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
};
}
String get key => jsondata['key'] ?? '';
String get key => (jsondata["key"] ?? "") as String;
String get testName => jsondata['test'] ?? '';
String get testName => (jsondata["test"] ?? "") as String;
bool get result => jsondata['result'] ?? false;
bool get result => (jsondata["result"] ?? false) as bool;
String get value => jsondata['value'] ?? '';
String get value => (jsondata["value"] ?? "") as String;
String get notes => jsondata['notes'] ?? '';
String get attachment => (jsondata["attachment"] ?? "") as String;
String get attachment => jsondata['attachment'] ?? '';
String get date => jsondata['date'] ?? '';
InvenTreeStockItemTestResult() : super();
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
String get date => (jsondata["date"] ?? "") as String;
@override
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
@ -60,6 +58,10 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
class InvenTreeStockItem extends InvenTreeModel {
InvenTreeStockItem() : super();
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
// Stock status codes
static const int OK = 10;
static const int ATTENTION = 50;
@ -97,7 +99,7 @@ class InvenTreeStockItem extends InvenTreeModel {
Color get statusColor {
switch (status) {
case OK:
return Color(0xFF50aa51);
return Colors.black;
case ATTENTION:
return Color(0xFFfdc82a);
case DAMAGED:
@ -114,7 +116,7 @@ class InvenTreeStockItem extends InvenTreeModel {
String get URL => "stock/";
@override
String WEB_URL = "stock/item/";
String get WEB_URL => "stock/item/";
@override
Map<String, dynamic> formFields() {
@ -132,33 +134,24 @@ class InvenTreeStockItem extends InvenTreeModel {
@override
Map<String, String> defaultGetFilters() {
var headers = new Map<String, String>();
headers["part_detail"] = "true";
headers["location_detail"] = "true";
headers["supplier_detail"] = "true";
headers["cascade"] = "false";
return headers;
return {
"part_detail": "true",
"location_detail": "true",
"supplier_detail": "true",
"cascade": "false"
};
}
@override
Map<String, String> defaultListFilters() {
var headers = new Map<String, String>();
headers["part_detail"] = "true";
headers["location_detail"] = "true";
headers["supplier_detail"] = "true";
headers["cascade"] = "false";
return headers;
}
InvenTreeStockItem() : super();
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
// TODO
return {
"part_detail": "true",
"location_detail": "true",
"supplier_detail": "true",
"cascade": "false",
"in_stock": "true",
};
}
List<InvenTreePartTestTemplate> testTemplates = [];
@ -204,17 +197,17 @@ class InvenTreeStockItem extends InvenTreeModel {
});
}
String get uid => jsondata['uid'] ?? '';
String get uid => (jsondata["uid"] ?? "") as String;
int get status => jsondata['status'] ?? -1;
int get status => (jsondata["status"] ?? -1) as int;
String get packaging => jsondata["packaging"] ?? "";
String get packaging => (jsondata["packaging"] ?? "") as String;
String get batch => jsondata["batch"] ?? "";
String get batch => (jsondata["batch"] ?? "") as String;
int get partId => jsondata['part'] ?? -1;
int get partId => (jsondata["part"] ?? -1) as int;
String get purchasePrice => jsondata['purchase_price'] ?? "";
String get purchasePrice => (jsondata["purchase_price"] ?? "") as String;
bool get hasPurchasePrice {
@ -223,12 +216,14 @@ class InvenTreeStockItem extends InvenTreeModel {
return pp.isNotEmpty && pp.trim() != "-";
}
int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
int get purchaseOrderId => (jsondata["purchase_order"] ?? -1) as int;
int get trackingItemCount => (jsondata["tracking_items"] ?? 0) as int;
// Date of last update
DateTime? get updatedDate {
if (jsondata.containsKey("updated")) {
return DateTime.tryParse(jsondata["updated"] ?? '');
return DateTime.tryParse((jsondata["updated"] ?? "") as String);
} else {
return null;
}
@ -248,7 +243,7 @@ class InvenTreeStockItem extends InvenTreeModel {
DateTime? get stocktakeDate {
if (jsondata.containsKey("stocktake_date")) {
return DateTime.tryParse(jsondata["stocktake_date"] ?? '');
return DateTime.tryParse((jsondata["stocktake_date"] ?? "") as String);
} else {
return null;
}
@ -268,45 +263,45 @@ class InvenTreeStockItem extends InvenTreeModel {
String get partName {
String nm = '';
String nm = "";
// Use the detailed part information as priority
if (jsondata.containsKey('part_detail')) {
nm = jsondata['part_detail']['full_name'] ?? '';
if (jsondata.containsKey("part_detail")) {
nm = (jsondata["part_detail"]["full_name"] ?? "") as String;
}
// Backup if first value fails
if (nm.isEmpty) {
nm = jsondata['part__name'] ?? '';
nm = (jsondata["part__name"] ?? "") as String;
}
return nm;
}
String get partDescription {
String desc = '';
String desc = "";
// Use the detailed part description as priority
if (jsondata.containsKey('part_detail')) {
desc = jsondata['part_detail']['description'] ?? '';
if (jsondata.containsKey("part_detail")) {
desc = (jsondata["part_detail"]["description"] ?? "") as String;
}
if (desc.isEmpty) {
desc = jsondata['part__description'] ?? '';
desc = (jsondata["part__description"] ?? "") as String;
}
return desc;
}
String get partImage {
String img = '';
String img = "";
if (jsondata.containsKey('part_detail')) {
img = jsondata['part_detail']['thumbnail'] ?? '';
if (jsondata.containsKey("part_detail")) {
img = (jsondata["part_detail"]["thumbnail"] ?? "") as String;
}
if (img.isEmpty) {
img = jsondata['part__thumbnail'] ?? '';
img = (jsondata["part__thumbnail"] ?? "") as String;
}
return img;
@ -319,107 +314,97 @@ class InvenTreeStockItem extends InvenTreeModel {
String thumb = "";
thumb = jsondata['part_detail']?['thumbnail'] ?? '';
thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
// Use 'image' as a backup
// Use "image" as a backup
if (thumb.isEmpty) {
thumb = jsondata['part_detail']?['image'] ?? '';
thumb = (jsondata["part_detail"]?["image"] ?? "") as String;
}
// Try a different approach
if (thumb.isEmpty) {
thumb = jsondata['part__thumbnail'] ?? '';
thumb = (jsondata["part__thumbnail"] ?? "") as String;
}
// Still no thumbnail? Use the 'no image' image
// Still no thumbnail? Use the "no image" image
if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb;
return thumb;
}
int get supplierPartId => (jsondata['supplier_part'] ?? -1) as int;
int get supplierPartId => (jsondata["supplier_part"] ?? -1) as int;
String get supplierImage {
String thumb = '';
String thumb = "";
if (jsondata.containsKey("supplier_detail")) {
thumb = jsondata['supplier_detail']['supplier_logo'] ?? '';
thumb = (jsondata["supplier_detail"]["supplier_logo"] ?? "") as String;
}
return thumb;
}
String get supplierName {
String sname = '';
String sname = "";
if (jsondata.containsKey("supplier_detail")) {
sname = jsondata["supplier_detail"]["supplier_name"] ?? '';
sname = (jsondata["supplier_detail"]["supplier_name"] ?? "") as String;
}
return sname;
}
String get units {
return jsondata['part_detail']?['units'] ?? '';
return (jsondata["part_detail"]?["units"] ?? "") as String;
}
String get supplierSKU {
String sku = '';
String sku = "";
if (jsondata.containsKey("supplier_detail")) {
sku = jsondata["supplier_detail"]["SKU"] ?? '';
sku = (jsondata["supplier_detail"]["SKU"] ?? "") as String;
}
return sku;
}
String get serialNumber => jsondata['serial'] ?? "";
String get serialNumber => (jsondata["serial"] ?? "") as String;
double get quantity => double.tryParse(jsondata['quantity'].toString()) ?? 0;
double get quantity => double.tryParse(jsondata["quantity"].toString()) ?? 0;
String get quantityString {
String quantityString({bool includeUnits = false}){
String q = quantity.toString();
String q = simpleNumberString(quantity);
// Simplify integer values e.g. "1.0" becomes "1"
if (quantity.toInt() == quantity) {
q = quantity.toInt().toString();
}
if (units.isNotEmpty) {
if (includeUnits && units.isNotEmpty) {
q += " ${units}";
}
return q;
}
int get locationId => (jsondata['location'] ?? -1) as int;
int get locationId => (jsondata["location"] ?? -1) as int;
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
String serialOrQuantityDisplay() {
if (isSerialized()) {
return 'SN ${serialNumber}';
return "SN ${serialNumber}";
}
// Is an integer?
if (quantity.toInt() == quantity) {
return '${quantity.toInt()}';
}
return '${quantity}';
return simpleNumberString(quantity);
}
String get locationName {
String loc = '';
String loc = "";
if (locationId == -1 || !jsondata.containsKey('location_detail')) return 'Unknown Location';
if (locationId == -1 || !jsondata.containsKey("location_detail")) return "Unknown Location";
loc = jsondata['location_detail']['name'] ?? '';
loc = (jsondata["location_detail"]["name"] ?? "") as String;
// Old-style name
if (loc.isEmpty) {
loc = jsondata['location__name'] ?? '';
loc = (jsondata["location__name"] ?? "") as String;
}
return loc;
@ -427,9 +412,9 @@ class InvenTreeStockItem extends InvenTreeModel {
String get locationPathString {
if (locationId == -1 || !jsondata.containsKey('location_detail')) return L10().locationNotSet;
if (locationId == -1 || !jsondata.containsKey("location_detail")) return L10().locationNotSet;
String _loc = jsondata['location_detail']['pathstring'] ?? '';
String _loc = (jsondata["location_detail"]["pathstring"] ?? "") as String;
if (_loc.isNotEmpty) {
return _loc;
@ -444,7 +429,7 @@ class InvenTreeStockItem extends InvenTreeModel {
if (serialNumber.isNotEmpty) {
return "SN: $serialNumber";
} else {
return quantityString;
return simpleNumberString(quantity);
}
}
@ -481,7 +466,7 @@ class InvenTreeStockItem extends InvenTreeModel {
"pk": "${pk}",
"quantity": "${q}",
},
"notes": notes ?? '',
"notes": notes ?? "",
},
expectedStatusCode: 200
);
@ -489,6 +474,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return response.isValid();
}
// TODO: Refactor this once the server supports API metadata for this action
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/count/", q, notes: notes);
@ -496,6 +482,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return result;
}
// TODO: Refactor this once the server supports API metadata for this action
Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
@ -503,6 +490,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return result;
}
// TODO: Refactor this once the server supports API metadata for this action
Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
@ -510,6 +498,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return result;
}
// TODO: Refactor this once the server supports API metadata for this action
Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) {
quantity = this.quantity;
@ -535,10 +524,14 @@ class InvenTreeStockItem extends InvenTreeModel {
class InvenTreeStockLocation extends InvenTreeModel {
InvenTreeStockLocation() : super();
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "stock/location/";
String get pathstring => jsondata['pathstring'] ?? '';
String get pathstring => (jsondata["pathstring"] ?? "") as String;
@override
Map<String, dynamic> formFields() {
@ -551,13 +544,13 @@ class InvenTreeStockLocation extends InvenTreeModel {
String get parentpathstring {
// TODO - Drive the refactor tractor through this
List<String> psplit = pathstring.split('/');
List<String> psplit = pathstring.split("/");
if (psplit.length > 0) {
if (psplit.isNotEmpty) {
psplit.removeLast();
}
String p = psplit.join('/');
String p = psplit.join("/");
if (p.isEmpty) {
p = "Top level stock location";
@ -566,11 +559,7 @@ class InvenTreeStockLocation extends InvenTreeModel {
return p;
}
int get itemcount => jsondata['items'] ?? 0;
InvenTreeStockLocation() : super();
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
int get itemcount => (jsondata["items"] ?? 0) as int;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) {

View File

@ -1,8 +1,8 @@
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations_en.dart';
import "package:flutter_gen/gen_l10n/app_localizations.dart";
import "package:flutter_gen/gen_l10n/app_localizations_en.dart";
import 'package:one_context/one_context.dart';
import 'package:flutter/material.dart';
import "package:one_context/one_context.dart";
import "package:flutter/material.dart";
// Shortcut function to reduce boilerplate!
I18N L10()

@ -1 +1 @@
Subproject commit 3c7806d03887b8380efa22b8c1ca0e3eca2b98ad
Subproject commit d004dc013edfc47b5ed94c5b019a013dc5ef444a

View File

@ -1,19 +1,18 @@
import 'dart:async';
import "dart:async";
import 'package:inventree/inventree/sentry.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import "package:flutter_localizations/flutter_localizations.dart";
import "package:flutter_gen/gen_l10n/app_localizations.dart";
import 'package:inventree/widget/home.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:one_context/one_context.dart';
import 'package:package_info_plus/package_info_plus.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:one_context/one_context.dart";
import "package:package_info_plus/package_info_plus.dart";
import "package:flutter/foundation.dart";
import "package:sentry_flutter/sentry_flutter.dart";
import 'dsn.dart';
import 'package:flutter/foundation.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import "package:inventree/inventree/sentry.dart";
import "package:inventree/dsn.dart";
import "package:inventree/widget/home.dart";
Future<void> main() async {
@ -75,24 +74,24 @@ class InvenTreeApp extends StatelessWidget {
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('de', ''), // German
const Locale('el', ''), // Greek
const Locale('en', ''), // English
const Locale('es', ''), // Spanish
const Locale('fr', ''), // French
const Locale('he', ''), // Hebrew
const Locale('it', ''), // Italian
const Locale('ja', ''), // Japanese
const Locale('ko', ''), // Korean
const Locale('nl', ''), // Dutch
const Locale('no', ''), // Norwegian
const Locale('pl', ''), // Polish
const Locale('ru', ''), // Russian
const Locale('sv', ''), // Swedish
const Locale('th', ''), // Thai
const Locale('tr', ''), // Turkish
const Locale('vi', ''), // Vietnamese
const Locale('zh-CN', ''), // Chinese
const Locale("de", ""), // German
const Locale("el", ""), // Greek
const Locale("en", ""), // English
const Locale("es", ""), // Spanish
const Locale("fr", ""), // French
const Locale("he", ""), // Hebrew
const Locale("it", ""), // Italian
const Locale("ja", ""), // Japanese
const Locale("ko", ""), // Korean
const Locale("nl", ""), // Dutch
const Locale("no", ""), // Norwegian
const Locale("pl", ""), // Polish
const Locale("ru", ""), // Russian
const Locale("sv", ""), // Swedish
const Locale("th", ""), // Thai
const Locale("tr", ""), // Turkish
const Locale("vi", ""), // Vietnamese
const Locale("zh-CN", ""), // Chinese
],
);

View File

@ -1,20 +1,22 @@
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
import 'package:path/path.dart';
import 'dart:async';
import "dart:async";
import "package:path_provider/path_provider.dart";
import "package:sembast/sembast.dart";
import "package:sembast/sembast_io.dart";
import "package:path/path.dart";
/*
* Class for storing InvenTree preferences in a NoSql DB
*/
class InvenTreePreferencesDB {
InvenTreePreferencesDB._();
static final InvenTreePreferencesDB _singleton = InvenTreePreferencesDB._();
static InvenTreePreferencesDB get instance => _singleton;
InvenTreePreferencesDB._();
Completer<Database> _dbOpenCompleter = Completer();
bool isOpen = false;
@ -34,7 +36,7 @@ class InvenTreePreferencesDB {
return _dbOpenCompleter.future;
}
Future _openDatabase() async {
Future<void> _openDatabase() async {
// Get a platform-specific directory where persistent app data can be stored
final appDocumentDir = await getApplicationDocumentsDirectory();
@ -43,7 +45,7 @@ class InvenTreePreferencesDB {
print("Path: ${appDocumentDir.path}");
// Path with the form: /platform-specific-directory/demo.db
final dbPath = join(appDocumentDir.path, 'InvenTreeSettings.db');
final dbPath = join(appDocumentDir.path, "InvenTreeSettings.db");
final database = await databaseFactoryIo.openDatabase(dbPath);
@ -54,8 +56,14 @@ class InvenTreePreferencesDB {
class InvenTreePreferences {
factory InvenTreePreferences() {
return _api;
}
InvenTreePreferences._internal();
/* The following settings are not stored to persistent storage,
* instead they are only used as 'session preferences'.
* instead they are only used as "session preferences".
* They are kept here as a convenience only.
*/
@ -72,11 +80,6 @@ class InvenTreePreferences {
bool expandStockList = true;
// Ensure we only ever create a single instance of the preferences class
static final InvenTreePreferences _api = new InvenTreePreferences._internal();
static final InvenTreePreferences _api = InvenTreePreferences._internal();
factory InvenTreePreferences() {
return _api;
}
InvenTreePreferences._internal();
}

View File

@ -1,22 +1,22 @@
import 'package:inventree/api.dart';
import 'package:inventree/app_colors.dart';
import 'package:inventree/settings/release.dart';
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/settings/release.dart";
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:package_info_plus/package_info_plus.dart";
import 'package:inventree/l10.dart';
import "package:inventree/l10.dart";
class InvenTreeAboutWidget extends StatelessWidget {
const InvenTreeAboutWidget(this.info) : super();
final PackageInfo info;
InvenTreeAboutWidget(this.info) : super();
void _releaseNotes(BuildContext context) async {
Future <void> _releaseNotes(BuildContext context) async {
// Load release notes from external file
String notes = await rootBundle.loadString("assets/release_notes.md");
@ -27,7 +27,7 @@ class InvenTreeAboutWidget extends StatelessWidget {
);
}
void _credits(BuildContext context) async {
Future <void> _credits(BuildContext context) async {
String notes = await rootBundle.loadString("assets/credits.md");

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import "package:flutter/material.dart";
import "package:flutter/cupertino.dart";
import 'package:inventree/l10.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_settings.dart';
import "package:inventree/app_settings.dart";
class InvenTreeAppSettingsWidget extends StatefulWidget {
@override
@ -15,10 +15,10 @@ class InvenTreeAppSettingsWidget extends StatefulWidget {
class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
final GlobalKey<_InvenTreeAppSettingsState> _settingsKey = GlobalKey<_InvenTreeAppSettingsState>();
_InvenTreeAppSettingsState();
final GlobalKey<_InvenTreeAppSettingsState> _settingsKey = GlobalKey<_InvenTreeAppSettingsState>();
bool barcodeSounds = true;
bool serverSounds = true;
bool partSubcategory = false;
@ -31,7 +31,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
loadSettings();
}
void loadSettings() async {
Future <void> loadSettings() async {
barcodeSounds = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
serverSounds = await InvenTreeSettingsManager().getValue("serverSounds", true) as bool;
@ -42,35 +42,35 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
});
}
void setBarcodeSounds(bool en) async {
Future <void> setBarcodeSounds(bool en) async {
await InvenTreeSettingsManager().setValue("barcodeSounds", en);
barcodeSounds = await InvenTreeSettingsManager().getValue("barcodeSounds", true);
barcodeSounds = await InvenTreeSettingsManager().getBool("barcodeSounds", true);
setState(() {
});
}
void setServerSounds(bool en) async {
Future <void> setServerSounds(bool en) async {
await InvenTreeSettingsManager().setValue("serverSounds", en);
serverSounds = await InvenTreeSettingsManager().getValue("serverSounds", true);
serverSounds = await InvenTreeSettingsManager().getBool("serverSounds", true);
setState(() {
});
}
void setPartSubcategory(bool en) async {
Future <void> setPartSubcategory(bool en) async {
await InvenTreeSettingsManager().setValue("partSubcategory", en);
partSubcategory = await InvenTreeSettingsManager().getValue("partSubcategory", true);
partSubcategory = await InvenTreeSettingsManager().getBool("partSubcategory", true);
setState(() {
});
}
void setStockSublocation(bool en) async {
Future <void> setStockSublocation(bool en) async {
await InvenTreeSettingsManager().setValue("stockSublocation", en);
stockSublocation = await InvenTreeSettingsManager().getValue("stockSublocation", true);
stockSublocation = await InvenTreeSettingsManager().getBool("stockSublocation", true);
setState(() {
});

View File

@ -1,15 +1,12 @@
import 'package:inventree/app_colors.dart';
import 'package:inventree/widget/dialogs.dart';
import 'package:inventree/widget/fields.dart';
import 'package:inventree/widget/spinner.dart';
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/l10.dart';
import '../api.dart';
import '../user_profile.dart';
import "package:inventree/app_colors.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/spinner.dart";
import "package:inventree/l10.dart";
import "package:inventree/api.dart";
import "package:inventree/user_profile.dart";
class InvenTreeLoginSettingsWidget extends StatefulWidget {
@ -20,17 +17,15 @@ class InvenTreeLoginSettingsWidget extends StatefulWidget {
class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>();
final GlobalKey<FormState> _addProfileKey = new GlobalKey<FormState>();
List<UserProfile> profiles = [];
_InvenTreeLoginSettingsState() {
_reload();
}
void _reload() async {
final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>();
List<UserProfile> profiles = [];
Future <void> _reload() async {
profiles = await UserProfileDBManager().getAllProfiles();
@ -40,17 +35,6 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) {
var _name;
var _server;
var _username;
var _password;
UserProfile? profile;
if (userProfile != null) {
profile = userProfile;
}
Navigator.push(
context,
MaterialPageRoute(
@ -61,7 +45,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
});
}
void _selectProfile(BuildContext context, UserProfile profile) async {
Future <void> _selectProfile(BuildContext context, UserProfile profile) async {
// Disconnect InvenTree
InvenTreeAPI().disconnectFromServer();
@ -84,42 +68,24 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
_reload();
}
void _deleteProfile(UserProfile profile) async {
Future <void> _deleteProfile(UserProfile profile) async {
await UserProfileDBManager().deleteProfile(profile);
_reload();
if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? "")) {
InvenTreeAPI().disconnectFromServer();
}
}
void _updateProfile(UserProfile? profile) async {
if (profile == null) {
return;
}
_reload();
if (InvenTreeAPI().isConnected() && InvenTreeAPI().profile != null && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
// Attempt server login (this will load the newly selected profile
InvenTreeAPI().connectToServer().then((result) {
_reload();
});
}
}
Widget? _getProfileIcon(UserProfile profile) {
// Not selected? No icon for you!
if (!profile.selected) return null;
// Selected, but (for some reason) not the same as the API...
if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
if ((InvenTreeAPI().profile?.key ?? "") != profile.key) {
return FaIcon(
FontAwesomeIcons.questionCircle,
color: COLOR_WARNING
@ -150,7 +116,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
List<Widget> children = [];
if (profiles.length > 0) {
if (profiles.isNotEmpty) {
for (int idx = 0; idx < profiles.length; idx++) {
UserProfile profile = profiles[idx];
@ -253,9 +219,9 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
class ProfileEditWidget extends StatefulWidget {
UserProfile? profile;
const ProfileEditWidget(this.profile) : super();
ProfileEditWidget(this.profile) : super();
final UserProfile? profile;
@override
_ProfileEditState createState() => _ProfileEditState(profile);
@ -263,11 +229,11 @@ class ProfileEditWidget extends StatefulWidget {
class _ProfileEditState extends State<ProfileEditWidget> {
UserProfile? profile;
_ProfileEditState(this.profile) : super();
final formKey = new GlobalKey<FormState>();
UserProfile? profile;
final formKey = GlobalKey<FormState>();
String name = "";
String server = "";
@ -375,7 +341,7 @@ class _ProfileEditState extends State<ProfileEditWidget> {
if (uri.hasScheme) {
print("Scheme: ${uri.scheme}");
if (!(["http", "https"].contains(uri.scheme.toLowerCase()))) {
if (!["http", "https"].contains(uri.scheme.toLowerCase())) {
return L10().serverStart;
}
} else {

View File

@ -1,14 +1,14 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:inventree/l10.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter_markdown/flutter_markdown.dart";
import "package:inventree/l10.dart";
class ReleaseNotesWidget extends StatelessWidget {
final String releaseNotes;
const ReleaseNotesWidget(this.releaseNotes);
ReleaseNotesWidget(this.releaseNotes);
final String releaseNotes;
@override
Widget build (BuildContext context) {
@ -27,9 +27,9 @@ class ReleaseNotesWidget extends StatelessWidget {
class CreditsWidget extends StatelessWidget {
final String credits;
const CreditsWidget(this.credits);
CreditsWidget(this.credits);
final String credits;
@override
Widget build (BuildContext context) {

View File

@ -1,18 +1,16 @@
import 'package:inventree/app_colors.dart';
import 'package:inventree/settings/about.dart';
import 'package:inventree/settings/app_settings.dart';
import 'package:inventree/settings/login.dart';
import "package:inventree/app_colors.dart";
import "package:inventree/settings/about.dart";
import "package:inventree/settings/app_settings.dart";
import "package:inventree/settings/login.dart";
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/l10.dart';
import 'package:inventree/widget/submit_feedback.dart';
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/submit_feedback.dart";
import 'package:url_launcher/url_launcher.dart';
import "package:url_launcher/url_launcher.dart";
import 'login.dart';
import 'package:package_info_plus/package_info_plus.dart';
import "package:package_info_plus/package_info_plus.dart";
class InvenTreeSettingsWidget extends StatefulWidget {
// InvenTree settings view
@ -95,30 +93,30 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
}
void _openDocs() async {
Future <void> _openDocs() async {
if (await canLaunch(docsUrl)) {
await launch(docsUrl);
}
}
void _translate() async {
final String url = "https://crowdin.com/project/inventree";
Future <void> _translate() async {
const String url = "https://crowdin.com/project/inventree";
if (await canLaunch(url)) {
await launch(url);
}
}
void _editServerSettings() async {
Future <void> _editServerSettings() async {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget()));
}
void _editAppSettings() async {
Future <void> _editAppSettings() async {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeAppSettingsWidget()));
}
void _about() async {
Future <void> _about() async {
PackageInfo.fromPlatform().then((PackageInfo info) {
Navigator.push(context,
@ -126,7 +124,7 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
});
}
void _submitFeedback(BuildContext context) async {
Future <void> _submitFeedback(BuildContext context) async {
Navigator.push(
context,

View File

@ -2,8 +2,8 @@
/*
* Class for InvenTree user / login details
*/
import 'package:sembast/sembast.dart';
import 'preferences.dart';
import "package:sembast/sembast.dart";
import "preferences.dart";
class UserProfile {
@ -16,6 +16,15 @@ class UserProfile {
this.selected = false,
});
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
key: key,
name: json["name"] as String,
server: json["server"] as String,
username: json["username"] as String,
password: json["password"] as String,
selected: isSelected,
);
// ID of the profile
int? key;
@ -36,15 +45,6 @@ class UserProfile {
// User ID (will be provided by the server on log-in)
int user_id = -1;
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
key: key,
name: json['name'],
server: json['server'],
username: json['username'],
password: json['password'],
selected: isSelected,
);
Map<String, dynamic> toJson() => {
"name": name,
"server": server,
@ -62,7 +62,7 @@ class UserProfileDBManager {
final store = StoreRef("profiles");
Future<Database> get _db async => await InvenTreePreferencesDB.instance.database;
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
Future<bool> profileNameExists(String name) async {
@ -70,10 +70,10 @@ class UserProfileDBManager {
final profiles = await store.find(await _db, finder: finder);
return profiles.length > 0;
return profiles.isNotEmpty;
}
Future addProfile(UserProfile profile) async {
Future<void> addProfile(UserProfile profile) async {
// Check if a profile already exists with the name
final bool exists = await profileNameExists(profile.name);
@ -83,7 +83,7 @@ class UserProfileDBManager {
return;
}
int key = await store.add(await _db, profile.toJson());
int key = await store.add(await _db, profile.toJson()) as int;
print("Added user profile <${key}> - '${profile.name}'");
@ -91,7 +91,7 @@ class UserProfileDBManager {
profile.key = key;
}
Future selectProfile(int key) async {
Future<void> selectProfile(int key) async {
/*
* Mark the particular profile as selected
*/
@ -101,7 +101,7 @@ class UserProfileDBManager {
return result;
}
Future updateProfile(UserProfile profile) async {
Future<void> updateProfile(UserProfile profile) async {
if (profile.key == null) {
await addProfile(profile);
@ -115,7 +115,7 @@ class UserProfileDBManager {
return result;
}
Future deleteProfile(UserProfile profile) async {
Future<void> deleteProfile(UserProfile profile) async {
await store.record(profile.key).delete(await _db);
print("Deleted user profile <${profile.key}> - '${profile.name}'");
}
@ -135,8 +135,8 @@ class UserProfileDBManager {
if (profiles[idx].key is int && profiles[idx].key == selected) {
return UserProfile.fromJson(
profiles[idx].key,
profiles[idx].value,
profiles[idx].key as int,
profiles[idx].value as Map<String, dynamic>,
profiles[idx].key == selected,
);
}
@ -161,8 +161,8 @@ class UserProfileDBManager {
if (profiles[idx].key is int) {
profileList.add(
UserProfile.fromJson(
profiles[idx].key,
profiles[idx].value,
profiles[idx].key as int,
profiles[idx].value as Map<String, dynamic>,
profiles[idx].key == selected,
));
}

27
lib/widget/back.dart Normal file
View File

@ -0,0 +1,27 @@
/*
* A custom implementation of a "Back" button for display in the app drawer
*
* Long-pressing on this will return the user to the home screen
*/
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
Widget backButton(BuildContext context, GlobalKey<ScaffoldState> key) {
return GestureDetector(
onLongPress: () {
// Display the menu
key.currentState!.openDrawer();
print("hello?");
},
child: IconButton(
icon: BackButtonIcon(),
onPressed: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
),
);
}

View File

@ -1,27 +1,22 @@
import "package:flutter/cupertino.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import 'package:inventree/api.dart';
import 'package:inventree/app_colors.dart';
import 'package:inventree/app_settings.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/inventree/sentry.dart';
import 'package:inventree/widget/progress.dart';
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/l10.dart';
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/part_list.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/refreshable_state.dart";
import 'package:inventree/widget/part_detail.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:inventree/widget/paginator.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class CategoryDisplayWidget extends StatefulWidget {
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
const CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
final InvenTreePartCategory? category;
@ -32,6 +27,7 @@ class CategoryDisplayWidget extends StatefulWidget {
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
_CategoryDisplayState(this.category);
@override
String getAppBarTitle(BuildContext context) => L10().partCategory;
@ -41,7 +37,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
List<Widget> actions = [];
if ((category != null) && InvenTreeAPI().checkPermission('part_category', 'change')) {
if ((category != null) && InvenTreeAPI().checkPermission("part_category", "change")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.edit),
@ -74,8 +70,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
);
}
_CategoryDisplayState(this.category);
// The local InvenTreePartCategory object
final InvenTreePartCategory? category;
@ -199,7 +193,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
if (loading) {
tiles.add(progressIndicator());
} else if (_subcategories.length == 0) {
} else if (_subcategories.isEmpty) {
tiles.add(ListTile(
title: Text(L10().noSubcategories),
subtitle: Text(
@ -224,8 +218,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
data: {
"parent": (pk > 0) ? pk : null,
},
onSuccess: (data) async {
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var cat = InvenTreePartCategory.fromJson(data);
@ -252,7 +248,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
data: {
"category": (pk > 0) ? pk : null
},
onSuccess: (data) async {
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var part = InvenTreePart.fromJson(data);
@ -274,7 +272,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
getCategoryDescriptionCard(extra: false),
];
if (InvenTreeAPI().checkPermission('part', 'add')) {
if (InvenTreeAPI().checkPermission("part", "add")) {
tiles.add(
ListTile(
title: Text(L10().categoryCreate),
@ -298,7 +296,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
}
}
if (tiles.length == 0) {
if (tiles.isEmpty) {
tiles.add(
ListTile(
title: Text(
@ -327,7 +325,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
);
case 1:
return PaginatedPartList(
{"category": "${category?.pk ?? null}"},
{
"category": "${category?.pk ?? 'null'}"
},
);
case 2:
return ListView(
@ -344,9 +344,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
* Builder for displaying a list of PartCategory objects
*/
class SubcategoryList extends StatelessWidget {
final List<InvenTreePartCategory> _categories;
SubcategoryList(this._categories);
const SubcategoryList(this._categories);
final List<InvenTreePartCategory> _categories;
void _openCategory(BuildContext context, int pk) {
@ -381,170 +382,3 @@ class SubcategoryList extends StatelessWidget {
itemBuilder: _build, itemCount: _categories.length);
}
}
/*
* Widget for displaying a list of Part objects within a PartCategory display.
*
* Uses server-side pagination for snappy results
*/
class PaginatedPartList extends StatefulWidget {
final Map<String, String> filters;
Function(int)? onTotalChanged;
PaginatedPartList(this.filters, {this.onTotalChanged});
@override
_PaginatedPartListState createState() => _PaginatedPartListState(filters, onTotalChanged);
}
class _PaginatedPartListState extends State<PaginatedPartList> {
static const _pageSize = 25;
String _searchTerm = "";
Function(int)? onTotalChanged;
final Map<String, String> filters;
_PaginatedPartListState(this.filters, this.onTotalChanged);
final PagingController<int, InvenTreePart> _pagingController = PagingController(firstPageKey: 0);
@override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
int resultCount = 0;
Future<void> _fetchPage(int pageKey) async {
try {
Map<String, String> params = filters;
params["search"] = _searchTerm;
final bool cascade = await InvenTreeSettingsManager().getValue("partSubcategory", true);
params["cascade"] = "${cascade}";
final page = await InvenTreePart().listPaginated(_pageSize, pageKey, filters: params);
int pageLength = page?.length ?? 0;
int pageCount = page?.count ?? 0;
final isLastPage = pageLength < _pageSize;
// Construct a list of part objects
List<InvenTreePart> parts = [];
if (page != null) {
for (var result in page.results) {
if (result is InvenTreePart) {
parts.add(result);
}
}
}
if (isLastPage) {
_pagingController.appendLastPage(parts);
} else {
final int nextPageKey = pageKey + pageLength;
_pagingController.appendPage(parts, nextPageKey);
}
if (onTotalChanged != null) {
onTotalChanged!(pageCount);
}
setState(() {
resultCount = pageCount;
});
} catch (error, stackTrace) {
print("Error! - ${error.toString()}");
_pagingController.error = error;
sentryReportError(error, stackTrace);
}
}
void _openPart(BuildContext context, int pk) {
// Attempt to load the part information
InvenTreePart().get(pk).then((var part) {
if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
}
});
}
Widget _buildPart(BuildContext context, InvenTreePart part) {
return ListTile(
title: Text(part.fullname),
subtitle: Text("${part.description}"),
trailing: Text("${part.inStockString}"),
leading: InvenTreeAPI().getImage(
part.thumbnail,
width: 40,
height: 40,
),
onTap: () {
_openPart(context, part.pk);
},
);
}
final TextEditingController searchController = TextEditingController();
void updateSearchTerm() {
_searchTerm = searchController.text;
_pagingController.refresh();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
Expanded(
child: CustomScrollView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
slivers: [
PagedSliverList.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreePart>(
itemBuilder: (context, item, index) {
return _buildPart(context, item);
},
noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget(L10().partNoResults);
},
),
separatorBuilder: (context, index) => const Divider(height: 1),
)
],
)
)
],
);
}
}

View File

@ -0,0 +1,81 @@
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/category_display.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
class PartCategoryList extends StatefulWidget {
const PartCategoryList(this.filters);
final Map<String, String> filters;
@override
_PartCategoryListState createState() => _PartCategoryListState(filters);
}
class _PartCategoryListState extends RefreshableState<PartCategoryList> {
_PartCategoryListState(this.filters);
final Map<String, String> filters;
@override
String getAppBarTitle(BuildContext context) => L10().partCategories;
@override
Widget getBody(BuildContext context) {
return PaginatedPartCategoryList(filters);
}
}
class PaginatedPartCategoryList extends StatefulWidget {
const PaginatedPartCategoryList(this.filters);
final Map<String, String> filters;
@override
_PaginatedPartCategoryListState createState() => _PaginatedPartCategoryListState(filters);
}
class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPartCategoryList> {
_PaginatedPartCategoryListState(Map<String, String> filters) : super(filters);
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreePartCategory().listPaginated(limit, offset, filters: params);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreePartCategory category = model as InvenTreePartCategory;
return ListTile(
title: Text(category.name),
subtitle: Text(category.pathstring),
trailing: Text("${category.partcount}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(category)
)
);
},
);
}
}

View File

@ -1,19 +1,21 @@
import 'package:inventree/api.dart';
import 'package:inventree/api_form.dart';
import 'package:inventree/app_colors.dart';
import 'package:inventree/inventree/company.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/l10.dart';
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/purchase_order_list.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/l10.dart";
class CompanyDetailWidget extends StatefulWidget {
final InvenTreeCompany company;
const CompanyDetailWidget(this.company, {Key? key}) : super(key: key);
CompanyDetailWidget(this.company, {Key? key}) : super(key: key);
final InvenTreeCompany company;
@override
_CompanyDetailState createState() => _CompanyDetailState(company);
@ -27,6 +29,8 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
final InvenTreeCompany company;
List<InvenTreePurchaseOrder> outstandingOrders = [];
@override
String getAppBarTitle(BuildContext context) => L10().company;
@ -61,9 +65,13 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
@override
Future<void> request() async {
await company.reload();
if (company.isSupplier) {
outstandingOrders = await company.getPurchaseOrders(outstanding: true);
}
}
void editCompany(BuildContext context) async {
Future <void> editCompany(BuildContext context) async {
company.editForm(
context,
@ -146,6 +154,37 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
// TODO - Add list of purchase orders
tiles.add(Divider());
tiles.add(
ListTile(
title: Text(L10().purchaseOrders),
leading: FaIcon(FontAwesomeIcons.shoppingCart, color: COLOR_CLICK),
trailing: Text("${outstandingOrders.length}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderListWidget(
filters: {
"supplier": "${company.pk}"
}
)
)
);
}
)
);
// TODO: Display "supplied parts" count (click through to list of supplier parts)
/*
tiles.add(
ListTile(
title: Text(L10().suppliedParts),
leading: FaIcon(FontAwesomeIcons.shapes),
trailing: Text("${company.partSuppliedCount}"),
)
);
*/
}
if (company.isManufacturer) {

View File

@ -1,25 +1,22 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import 'package:inventree/api.dart';
import 'package:inventree/inventree/company.dart';
import 'package:inventree/inventree/sentry.dart';
import 'package:inventree/widget/paginator.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:inventree/widget/company_detail.dart';
import '../l10.dart';
import "package:inventree/api.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/company_detail.dart";
class CompanyListWidget extends StatefulWidget {
CompanyListWidget(this.title, this.filters, {Key? key}) : super(key: key);
const CompanyListWidget(this.title, this.filters, {Key? key}) : super(key: key);
String title;
final String title;
Map<String, String> filters;
final Map<String, String> filters;
@override
_CompanyListWidgetState createState() => _CompanyListWidgetState(title, filters);
@ -49,103 +46,32 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
class PaginatedCompanyList extends StatefulWidget {
PaginatedCompanyList(this.filters, {this.onTotalChanged});
const PaginatedCompanyList(this.filters, {this.onTotalChanged});
final Map<String, String> filters;
Function(int)? onTotalChanged;
final Function(int)? onTotalChanged;
@override
_CompanyListState createState() => _CompanyListState(filters, onTotalChanged);
_CompanyListState createState() => _CompanyListState(filters);
}
class _CompanyListState extends State<PaginatedCompanyList> {
class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> {
_CompanyListState(this.filters, this.onTotalChanged);
static const _pageSize = 25;
_CompanyListState(Map<String, String> filters) : super(filters);
String _searchTerm = "";
Function(int)? onTotalChanged;
final Map<String, String> filters;
final PagingController<int, InvenTreeCompany> _pagingController = PagingController(firstPageKey: 0);
final TextEditingController searchController = TextEditingController();
@override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeCompany().listPaginated(limit, offset, filters: params);
return page;
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
int resultCount = 0;
Future<void> _fetchPage(int pageKey) async {
try {
Map<String, String> params = filters;
Widget buildItem(BuildContext context, InvenTreeModel model) {
params["search"] = _searchTerm;
final page = await InvenTreeCompany().listPaginated(
_pageSize, pageKey, filters: params);
int pageLength = page?.length ?? 0;
int pageCount = page?.count ?? 0;
final isLastPage = pageLength < _pageSize;
List<InvenTreeCompany> companies = [];
if (page != null) {
for (var result in page.results) {
if (result is InvenTreeCompany) {
companies.add(result);
} else {
print(result.jsondata);
}
}
}
if (isLastPage) {
_pagingController.appendLastPage(companies);
} else {
final int nextPageKey = pageKey + pageLength;
_pagingController.appendPage(companies, nextPageKey);
}
if (onTotalChanged != null) {
onTotalChanged!(pageCount);
}
setState(() {
resultCount = pageCount;
});
} catch (error, stackTrace) {
print("Error! - ${error.toString()}");
_pagingController.error = error;
sentryReportError(error, stackTrace);
}
}
void updateSearchTerm() {
_searchTerm = searchController.text;
_pagingController.refresh();
}
Widget _buildCompany(BuildContext context, InvenTreeCompany company) {
InvenTreeCompany company = model as InvenTreeCompany;
return ListTile(
title: Text(company.name),
@ -160,36 +86,4 @@ class _CompanyListState extends State<PaginatedCompanyList> {
},
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
Expanded(
child: CustomScrollView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
slivers: [
PagedSliverList.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreeCompany>(
itemBuilder: (context, item, index) {
return _buildCompany(context, item);
},
noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget(L10().companyNoResults);
}
),
separatorBuilder: (context, index) => const Divider(height: 1),
)
],
)
)
],
);
}
}

View File

@ -1,12 +1,12 @@
import 'package:inventree/app_settings.dart';
import 'package:inventree/widget/snacks.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/l10.dart';
import 'package:one_context/one_context.dart';
import "package:inventree/app_settings.dart";
import "package:inventree/widget/snacks.dart";
import "package:audioplayers/audioplayers.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/l10.dart";
import "package:one_context/one_context.dart";
Future<void> confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {

View File

@ -1,21 +1,17 @@
import 'package:inventree/api.dart';
import 'package:inventree/barcode.dart';
import 'package:inventree/widget/company_list.dart';
import 'package:inventree/widget/search.dart';
import 'package:flutter/material.dart';
import 'package:inventree/l10.dart';
import "package:inventree/api.dart";
import "package:inventree/barcode.dart";
import "package:flutter/material.dart";
import "package:inventree/l10.dart";
import 'package:inventree/widget/category_display.dart';
import 'package:inventree/widget/location_display.dart';
import 'package:inventree/settings/settings.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import "package:inventree/settings/settings.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/widget/search.dart";
class InvenTreeDrawer extends StatelessWidget {
final BuildContext context;
const InvenTreeDrawer(this.context);
InvenTreeDrawer(this.context);
final BuildContext context;
void _closeDrawer() {
// Close the drawer
@ -29,7 +25,9 @@ class InvenTreeDrawer extends StatelessWidget {
void _home() {
_closeDrawer();
Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false);
while (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
}
void _search() {
@ -38,65 +36,25 @@ class InvenTreeDrawer extends StatelessWidget {
_closeDrawer();
showSearch(
context: context,
delegate: PartSearchDelegate(context)
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchWidget()
)
);
//Navigator.push(context, MaterialPageRoute(builder: (context) => SearchWidget()));
}
/*
* Launch the camera to scan a QR code.
* Upon successful scan, data are passed off to be decoded.
*/
void _scan() async {
Future <void> _scan() async {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
scanQrCode(context);
}
/*
* Display the top-level PartCategory list
*/
void _showParts() {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
}
/*
* Display the top-level StockLocation list
*/
void _showStock() {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
}
void _showSuppliers() {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
}
void _showManufacturers() {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
}
void _showCustomers() {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
}
/*
* Load settings widget
*/
@ -107,17 +65,14 @@ class InvenTreeDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
children: ListTile.divideTiles(
context: context,
tiles: <Widget>[
ListTile(
leading: Image.asset(
"assets/image/icon.png",
fit: BoxFit.scaleDown,
width: 30,
),
leading: FaIcon(FontAwesomeIcons.home),
title: Text(
L10().appTitle,
style: TextStyle(fontWeight: FontWeight.bold),
@ -134,35 +89,6 @@ class InvenTreeDrawer extends StatelessWidget {
leading: FaIcon(FontAwesomeIcons.search),
onTap: _search,
),
ListTile(
title: Text(L10().parts),
leading: Icon(Icons.category),
onTap: _showParts,
),
ListTile(
title: Text(L10().stock),
leading: FaIcon(FontAwesomeIcons.boxes),
onTap: _showStock,
),
/*
ListTile(
title: Text("Suppliers"),
leading: FaIcon(FontAwesomeIcons.building),
onTap: _showSuppliers,
),
ListTile(
title: Text("Manufacturers"),
leading: FaIcon(FontAwesomeIcons.industry),
onTap: _showManufacturers,
),
ListTile(
title: Text("Customers"),
leading: FaIcon(FontAwesomeIcons.users),
onTap: _showCustomers,
),
*/
ListTile(
title: Text(L10().settings),
leading: Icon(Icons.settings),

View File

@ -1,15 +1,13 @@
import "dart:async";
import "dart:io";
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:image_picker/image_picker.dart';
import 'package:inventree/l10.dart';
import 'dart:async';
import 'dart:io';
import 'package:one_context/one_context.dart';
import "package:file_picker/file_picker.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:image_picker/image_picker.dart";
import "package:one_context/one_context.dart";
import "package:inventree/l10.dart";
class FilePickerDialog {
@ -167,7 +165,7 @@ class CheckBoxField extends FormField<bool> {
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(String?)? validator, bool allowEmpty = false, bool isEnabled = true}) :
super(
decoration: InputDecoration(
labelText: allowEmpty ? label : label + "*",
@ -182,7 +180,7 @@ class StringField extends TextFormField {
}
if (validator != null) {
return validator(value);
return validator(value) as String?;
}
return null;
@ -196,7 +194,7 @@ class StringField extends TextFormField {
*/
class QuantityField extends TextFormField {
QuantityField({String label = "", String hint = "", String initial = "", double? max, TextEditingController? controller}) :
QuantityField({String label = "", String hint = "", double? max, TextEditingController? controller}) :
super(
decoration: InputDecoration(
labelText: label,

View File

@ -1,27 +1,28 @@
import 'package:inventree/app_colors.dart';
import 'package:inventree/user_profile.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import "package:flutter/cupertino.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/settings/settings.dart";
import "package:inventree/user_profile.dart";
import "package:inventree/l10.dart";
import "package:inventree/barcode.dart";
import "package:inventree/api.dart";
import "package:inventree/settings/login.dart";
import "package:inventree/widget/category_display.dart";
import "package:inventree/widget/company_list.dart";
import "package:inventree/widget/location_display.dart";
import "package:inventree/widget/part_list.dart";
import "package:inventree/widget/purchase_order_list.dart";
import "package:inventree/widget/search.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/drawer.dart";
import 'package:inventree/barcode.dart';
import 'package:inventree/api.dart';
import 'package:inventree/settings/login.dart';
import 'package:inventree/widget/category_display.dart';
import 'package:inventree/widget/company_list.dart';
import 'package:inventree/widget/location_display.dart';
import 'package:inventree/widget/search.dart';
import 'package:inventree/widget/spinner.dart';
import 'package:inventree/widget/drawer.dart';
class InvenTreeHomePage extends StatefulWidget {
InvenTreeHomePage({Key? key}) : super(key: key);
const InvenTreeHomePage({Key? key}) : super(key: key);
@override
_InvenTreeHomePageState createState() => _InvenTreeHomePageState();
@ -29,33 +30,27 @@ class InvenTreeHomePage extends StatefulWidget {
class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
_InvenTreeHomePageState() : super() {
// Initially load the profile and attempt server connection
_loadProfile();
}
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
// Selected user profile
UserProfile? _profile;
void _searchParts() {
void _search(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return;
showSearch(
context: context,
delegate: PartSearchDelegate(context)
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchWidget()
)
);
}
void _searchStock() {
if (!InvenTreeAPI().checkConnection(context)) return;
showSearch(
context: context,
delegate: StockSearchDelegate(context)
);
}
void _scan(BuildContext context) {
@ -64,31 +59,60 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
scanQrCode(context);
}
void _parts(BuildContext context) {
void _showParts(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
}
void _stock(BuildContext context) {
void _showSettings(BuildContext context) {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget()));
}
void _showStarredParts(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartList({
"starred": "true"
})
)
);
}
void _showStock(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
}
void _suppliers() {
void _showPurchaseOrders(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderListWidget(filters: {})
)
);
}
void _showSuppliers(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
}
void _manufacturers() {
void _showManufacturers(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
}
void _customers() {
void _showCustomers(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
@ -103,7 +127,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
});
}
void _loadProfile() async {
Future <void> _loadProfile() async {
_profile = await UserProfileDBManager().getSelectedProfile();
@ -121,269 +145,180 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
setState(() {});
}
ListTile _serverTile() {
// No profile selected
// Tap to select / create a profile
if (_profile == null) {
return ListTile(
title: Text(L10().profileNotSelected),
subtitle: Text(L10().profileTapToCreate),
leading: FaIcon(FontAwesomeIcons.server),
trailing: FaIcon(
FontAwesomeIcons.user,
color: COLOR_DANGER,
),
onTap: () {
_selectProfile();
},
);
Widget _iconButton(BuildContext context, String label, IconData icon, {Function()? callback, String role = "", String permission = ""}) {
bool connected = InvenTreeAPI().isConnected();
bool allowed = true;
if (role.isNotEmpty || permission.isNotEmpty) {
allowed = InvenTreeAPI().checkPermission(role, permission);
}
// Profile is selected ...
if (InvenTreeAPI().isConnecting()) {
return ListTile(
title: Text(L10().serverConnecting),
subtitle: Text("${InvenTreeAPI().baseUrl}"),
leading: FaIcon(FontAwesomeIcons.server),
trailing: Spinner(
icon: FontAwesomeIcons.spinner,
color: COLOR_PROGRESS,
return GestureDetector(
child: Card(
margin: EdgeInsets.symmetric(
vertical: 10,
horizontal: 10
),
onTap: () {
_selectProfile();
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
icon,
color: connected && allowed ? COLOR_CLICK : Colors.grey,
),
Divider(
height: 12,
thickness: 0,
color: Colors.transparent,
),
Text(
label,
),
]
)
),
onTap: () {
if (!allowed) {
showSnackIcon(
L10().permissionRequired,
icon: FontAwesomeIcons.exclamationCircle,
success: false,
);
return;
}
);
} else if (InvenTreeAPI().isConnected()) {
return ListTile(
title: Text(L10().serverConnected),
subtitle: Text("${InvenTreeAPI().baseUrl}"),
leading: FaIcon(FontAwesomeIcons.server),
trailing: FaIcon(
FontAwesomeIcons.checkCircle,
color: COLOR_SUCCESS
),
onTap: () {
_selectProfile();
},
);
} else {
return ListTile(
title: Text(L10().serverCouldNotConnect),
subtitle: Text("${_profile!.server}"),
leading: FaIcon(FontAwesomeIcons.server),
trailing: FaIcon(
FontAwesomeIcons.timesCircle,
color: COLOR_DANGER,
),
onTap: () {
_selectProfile();
},
);
}
if (callback != null) {
callback();
}
},
);
}
List<Widget> getGridTiles(BuildContext context) {
return [
_iconButton(
context,
L10().scanBarcode,
FontAwesomeIcons.barcode,
callback: () {
_scan(context);
}
),
_iconButton(
context,
L10().search,
FontAwesomeIcons.search,
callback: () {
_search(context);
}
),
_iconButton(
context,
L10().parts,
FontAwesomeIcons.shapes,
callback: () {
_showParts(context);
}
),
_iconButton(
context,
L10().partsStarred,
FontAwesomeIcons.solidStar,
callback: () {
_showStarredParts(context);
}
),
_iconButton(
context,
L10().stock,
FontAwesomeIcons.boxes,
callback: () {
_showStock(context);
}
),
_iconButton(
context,
L10().purchaseOrders,
FontAwesomeIcons.shoppingCart,
callback: () {
_showPurchaseOrders(context);
}
),
/*
_iconButton(
context,
L10().salesOrders,
FontAwesomeIcons.truck,
),
*/
_iconButton(
context,
L10().suppliers,
FontAwesomeIcons.building,
callback: () {
_showSuppliers(context);
}
),
_iconButton(
context,
L10().manufacturers,
FontAwesomeIcons.industry,
callback: () {
_showManufacturers(context);
}
),
_iconButton(
context,
L10().customers,
FontAwesomeIcons.userTie,
callback: () {
_showCustomers(context);
}
),
_iconButton(
context,
L10().settings,
FontAwesomeIcons.cogs,
callback: () {
_showSettings(context);
}
)
];
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
key: _homeKey,
appBar: AppBar(
title: Text(L10().appTitle),
actions: <Widget>[
/*
actions: [
IconButton(
icon: FaIcon(FontAwesomeIcons.search),
tooltip: L10().search,
onPressed: _searchParts,
),
*/
icon: FaIcon(
FontAwesomeIcons.server,
color: InvenTreeAPI().isConnected() ? COLOR_SUCCESS : COLOR_DANGER,
),
onPressed: _selectProfile,
)
],
),
drawer: new InvenTreeDrawer(context),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: (<Widget>[
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.barcode),
tooltip: L10().scanBarcode,
onPressed: () { _scan(context); },
),
Text(L10().scanBarcode),
],
),
],
),
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.shapes),
tooltip: L10().parts,
onPressed: () { _parts(context); },
),
Text(L10().parts),
],
),
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.search),
tooltip: L10().searchParts,
onPressed: _searchParts,
),
Text(L10().searchParts),
],
),
// TODO - Re-add starred parts link
/*
Column(
children: <Widget>[
IconButton(
icon: FaIcon(FontAwesomeIcons.solidStar),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => StarredPartWidget()));
},
),
Text("Starred Parts"),
]
),
*/
],
),
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.boxes),
tooltip: L10().stock,
onPressed: () { _stock(context); },
),
Text(L10().stock),
],
),
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.search),
tooltip: L10().searchStock,
onPressed: _searchStock,
),
Text(L10().searchStock),
],
),
]
),
Spacer(),
// TODO - Re-add these when the features actually do something..
/*
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.building),
tooltip: "Suppliers",
onPressed: _suppliers,
),
Text("Suppliers"),
],
),
Column(
children: <Widget>[
IconButton(
icon: FaIcon(FontAwesomeIcons.industry),
tooltip: "Manufacturers",
onPressed: _manufacturers,
),
Text("Manufacturers")
],
),
Column(
children: <Widget>[
IconButton(
icon: FaIcon(FontAwesomeIcons.userTie),
tooltip: "Customers",
onPressed: _customers,
),
Text("Customers"),
]
)
],
),
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.tools),
tooltip: "Build",
onPressed: _unsupported,
),
Text("Build"),
],
),
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.shoppingCart),
tooltip: "Order",
onPressed: _unsupported,
),
Text("Order"),
]
),
Column(
children: <Widget>[
IconButton(
icon: new FaIcon(FontAwesomeIcons.truck),
tooltip: "Ship",
onPressed: _unsupported,
),
Text("Ship"),
]
)
],
),
Spacer(),
*/
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: _serverTile(),
),
],
),
]),
),
drawer: InvenTreeDrawer(context),
body: ListView(
children: [
GridView.extent(
maxCrossAxisExtent: 140,
shrinkWrap: true,
physics: ClampingScrollPhysics(),
children: getGridTiles(context),
),
],
),
);
}

View File

@ -1,21 +1,19 @@
import 'package:inventree/api.dart';
import 'package:inventree/app_colors.dart';
import 'package:inventree/app_settings.dart';
import 'package:inventree/barcode.dart';
import 'package:inventree/inventree/sentry.dart';
import 'package:inventree/inventree/stock.dart';
import 'package:inventree/widget/progress.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter/foundation.dart";
import 'package:inventree/widget/refreshable_state.dart';
import 'package:inventree/widget/stock_detail.dart';
import 'package:inventree/widget/paginator.dart';
import 'package:inventree/l10.dart';
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/barcode.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/stock_detail.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/stock_list.dart";
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class LocationDisplayWidget extends StatefulWidget {
@ -31,6 +29,8 @@ class LocationDisplayWidget extends StatefulWidget {
class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
_LocationDisplayState(this.location);
final InvenTreeStockLocation? location;
@override
@ -62,7 +62,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
);
*/
if ((location != null) && (InvenTreeAPI().checkPermission('stock_location', 'change'))) {
if ((location != null) && (InvenTreeAPI().checkPermission("stock_location", "change"))) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.edit),
@ -92,11 +92,9 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
);
}
_LocationDisplayState(this.location);
List<InvenTreeStockLocation> _sublocations = [];
String _locationFilter = '';
String _locationFilter = "";
List<InvenTreeStockLocation> get sublocations {
@ -146,7 +144,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
data: {
"parent": (pk > 0) ? pk : null,
},
onSuccess: (data) async {
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var loc = InvenTreeStockLocation.fromJson(data);
@ -175,7 +176,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
data: {
"location": pk,
},
onSuccess: (data) async {
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data);
@ -280,7 +284,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
children: detailTiles(),
);
case 1:
return PaginatedStockList(filters);
return PaginatedStockItemList(filters);
case 2:
return ListView(
children: ListTile.divideTiles(
@ -307,13 +311,13 @@ List<Widget> detailTiles() {
L10().sublocations,
style: TextStyle(fontWeight: FontWeight.bold),
),
trailing: sublocations.length > 0 ? Text("${sublocations.length}") : null,
trailing: sublocations.isNotEmpty ? Text("${sublocations.length}") : null,
),
];
if (loading) {
tiles.add(progressIndicator());
} else if (_sublocations.length > 0) {
} else if (_sublocations.isNotEmpty) {
tiles.add(SublocationList(_sublocations));
} else {
tiles.add(ListTile(
@ -334,7 +338,7 @@ List<Widget> detailTiles() {
tiles.add(locationDescriptionCard(includeActions: false));
if (InvenTreeAPI().checkPermission('stock', 'add')) {
if (InvenTreeAPI().checkPermission("stock", "add")) {
tiles.add(
ListTile(
@ -362,7 +366,7 @@ List<Widget> detailTiles() {
if (location != null) {
// Stock adjustment actions
if (InvenTreeAPI().checkPermission('stock', 'change')) {
if (InvenTreeAPI().checkPermission("stock", "change")) {
// Scan items into location
tiles.add(
ListTile(
@ -422,9 +426,10 @@ List<Widget> detailTiles() {
class SublocationList extends StatelessWidget {
final List<InvenTreeStockLocation> _locations;
SublocationList(this._locations);
const SublocationList(this._locations);
final List<InvenTreeStockLocation> _locations;
void _openLocation(BuildContext context, int pk) {
@ -440,7 +445,7 @@ class SublocationList extends StatelessWidget {
InvenTreeStockLocation loc = _locations[index];
return ListTile(
title: Text('${loc.name}'),
title: Text("${loc.name}"),
subtitle: Text("${loc.description}"),
trailing: Text("${loc.itemcount}"),
onTap: () {
@ -460,162 +465,3 @@ class SublocationList extends StatelessWidget {
);
}
}
/*
* Widget for displaying a list of stock items within a stock location.
*
* Users server-side pagination for snappy results
*/
class PaginatedStockList extends StatefulWidget {
final Map<String, String> filters;
PaginatedStockList(this.filters);
@override
_PaginatedStockListState createState() => _PaginatedStockListState(filters);
}
class _PaginatedStockListState extends State<PaginatedStockList> {
static const _pageSize = 25;
String _searchTerm = "";
final Map<String, String> filters;
_PaginatedStockListState(this.filters);
final PagingController<int, InvenTreeStockItem> _pagingController = PagingController(firstPageKey: 0);
@override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
int resultCount = 0;
Future<void> _fetchPage(int pageKey) async {
try {
Map<String, String> params = this.filters;
params["search"] = "${_searchTerm}";
// Do we include stock items from sub-locations?
final bool cascade = await InvenTreeSettingsManager().getValue("stockSublocation", true);
params["cascade"] = "${cascade}";
final page = await InvenTreeStockItem().listPaginated(_pageSize, pageKey, filters: params);
int pageLength = page?.length ?? 0;
int pageCount = page?.count ?? 0;
final isLastPage = pageLength < _pageSize;
// Construct a list of stock item objects
List<InvenTreeStockItem> items = [];
if (page != null) {
for (var result in page.results) {
if (result is InvenTreeStockItem) {
items.add(result);
}
}
}
if (isLastPage) {
_pagingController.appendLastPage(items);
} else {
final int nextPageKey = pageKey + pageLength;
_pagingController.appendPage(items, nextPageKey);
}
setState(() {
resultCount = pageCount;
});
} catch (error, stackTrace) {
_pagingController.error = error;
sentryReportError(error, stackTrace);
}
}
void _openItem(BuildContext context, int pk) {
InvenTreeStockItem().get(pk).then((var item) {
if (item is InvenTreeStockItem) {
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
}
});
}
Widget _buildItem(BuildContext context, InvenTreeStockItem item) {
return ListTile(
title: Text("${item.partName}"),
subtitle: Text("${item.locationPathString}"),
leading: InvenTreeAPI().getImage(
item.partThumbnail,
width: 40,
height: 40,
),
trailing: Text("${item.displayQuantity}",
style: TextStyle(fontWeight: FontWeight.bold),
),
onTap: () {
_openItem(context, item.pk);
},
);
}
final TextEditingController searchController = TextEditingController();
void updateSearchTerm() {
_searchTerm = searchController.text;
_pagingController.refresh();
}
@override
Widget build (BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
Expanded(
child: CustomScrollView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
slivers: <Widget>[
// TODO - Search input
PagedSliverList.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreeStockItem>(
itemBuilder: (context, item, index) {
return _buildItem(context, item);
},
noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget("No stock items found");
}
),
separatorBuilder: (context, item) => const Divider(height: 1),
)
]
)
)
]
);
}
}

View File

@ -0,0 +1,82 @@
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/location_display.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
class StockLocationList extends StatefulWidget {
const StockLocationList(this.filters);
final Map<String, String> filters;
@override
_StockLocationListState createState() => _StockLocationListState(filters);
}
class _StockLocationListState extends RefreshableState<StockLocationList> {
_StockLocationListState(this.filters);
final Map<String, String> filters;
@override
String getAppBarTitle(BuildContext context) => L10().stockLocations;
@override
Widget getBody(BuildContext context) {
return PaginatedStockLocationList(filters);
}
}
class PaginatedStockLocationList extends StatefulWidget {
const PaginatedStockLocationList(this.filters);
final Map<String, String> filters;
@override
_PaginatedStockLocationListState createState() => _PaginatedStockLocationListState(filters);
}
class _PaginatedStockLocationListState extends PaginatedSearchState<PaginatedStockLocationList> {
_PaginatedStockLocationListState(Map<String, String> filters) : super(filters);
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeStockLocation().listPaginated(limit, offset, filters: params);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeStockLocation location = model as InvenTreeStockLocation;
return ListTile(
title: Text(location.name),
subtitle: Text(location.pathstring),
trailing: Text("${location.itemcount}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LocationDisplayWidget(location)
)
);
},
);
}
}

View File

@ -1,19 +1,158 @@
// Pagination related widgets
import "package:flutter/material.dart";
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/l10.dart';
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:infinite_scroll_pagination/infinite_scroll_pagination.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/l10.dart";
class PaginatedSearchState<T extends StatefulWidget> extends State<T> {
PaginatedSearchState(this.filters);
final Map<String, String> filters;
static const _pageSize = 25;
// Search query term
String searchTerm = "";
int resultCount = 0;
// Text controller
final TextEditingController searchController = TextEditingController();
// Pagination controller
final PagingController<int, InvenTreeModel> _pagingController = PagingController(firstPageKey: 0);
@override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
print("Blank request page");
// Default implementation returns null - must be overridden
return null;
}
Future<void> _fetchPage(int pageKey) async {
try {
Map<String, String> params = filters;
params["search"] = "${searchTerm}";
final page = await requestPage(
_pageSize,
pageKey,
params
);
int pageLength = page?.length ?? 0;
int pageCount = page?.count ?? 0;
final isLastPage = pageLength < _pageSize;
List<InvenTreeModel> items = [];
if (page != null) {
for (var result in page.results) {
if (result is InvenTreeModel) {
items.add(result);
}
}
}
if (isLastPage) {
_pagingController.appendLastPage(items);
} else {
final int nextPageKey = pageKey + pageLength;
_pagingController.appendPage(items, nextPageKey);
}
setState(() {
resultCount = pageCount;
});
} catch (error, stackTrace) {
_pagingController.error = error;
sentryReportError(error, stackTrace);
}
}
void updateSearchTerm() {
searchTerm = searchController.text;
_pagingController.refresh();
}
Widget buildItem(BuildContext context, InvenTreeModel item) {
// This method must be overridden by the child class
return ListTile(
title: Text("*** UNIMPLEMENTED ***"),
subtitle: Text("*** buildItem() is unimplemented for this widget!"),
);
}
String get noResultsText => L10().noResults;
@override
Widget build (BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
Expanded(
child: CustomScrollView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
slivers: <Widget>[
// TODO - Search input
PagedSliverList.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>(
itemBuilder: (context, item, index) {
return buildItem(context, item);
},
noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget(noResultsText);
}
),
separatorBuilder: (context, item) => const Divider(height: 1),
)
]
)
)
]
);
}
}
class PaginatedSearchWidget extends StatelessWidget {
Function onChanged;
const PaginatedSearchWidget(this.controller, this.onChanged, this.results);
int results = 0;
final Function onChanged;
TextEditingController controller;
final int results;
PaginatedSearchWidget(this.controller, this.onChanged, this.results);
final TextEditingController controller;
@override
Widget build(BuildContext context) {
@ -44,9 +183,9 @@ class PaginatedSearchWidget extends StatelessWidget {
class NoResultsWidget extends StatelessWidget {
final String description;
const NoResultsWidget(this.description);
NoResultsWidget(this.description);
final String description;
@override
Widget build(BuildContext context) {

View File

@ -1,23 +1,19 @@
import "dart:io";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/fields.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:image_picker/image_picker.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/widget/fields.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:inventree/widget/snacks.dart';
import 'dart:io';
import '../api.dart';
import '../l10.dart';
import "package:inventree/api.dart";
import "package:inventree/l10.dart";
class PartAttachmentsWidget extends StatefulWidget {
PartAttachmentsWidget(this.part, {Key? key}) : super(key: key);
const PartAttachmentsWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part;
@ -42,7 +38,7 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('part', 'change')) {
if (InvenTreeAPI().checkPermission("part", "change")) {
// File upload
actions.add(
@ -127,7 +123,7 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
));
}
if (tiles.length == 0) {
if (tiles.isEmpty) {
tiles.add(ListTile(
title: Text(L10().attachmentNone),
subtitle: Text(

View File

@ -1,28 +1,28 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/app_colors.dart';
import 'package:inventree/inventree/stock.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import 'package:inventree/l10.dart';
import 'package:inventree/widget/part_attachments_widget.dart';
import 'package:inventree/widget/part_notes.dart';
import 'package:inventree/widget/progress.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/widget/category_display.dart';
import 'package:inventree/api.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:inventree/widget/part_image_widget.dart';
import 'package:inventree/widget/stock_detail.dart';
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'location_display.dart';
import "package:inventree/app_colors.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/part_attachments_widget.dart";
import "package:inventree/widget/part_notes.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/category_display.dart";
import "package:inventree/api.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/part_image_widget.dart";
import "package:inventree/widget/stock_detail.dart";
import "package:inventree/widget/stock_list.dart";
class PartDetailWidget extends StatefulWidget {
PartDetailWidget(this.part, {Key? key}) : super(key: key);
const PartDetailWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part;
@ -34,10 +34,10 @@ class PartDetailWidget extends StatefulWidget {
class _PartDisplayState extends RefreshableState<PartDetailWidget> {
InvenTreePart part;
_PartDisplayState(this.part);
InvenTreePart part;
@override
String getAppBarTitle(BuildContext context) => L10().partDetails;
@ -46,7 +46,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('part', 'view')) {
if (InvenTreeAPI().checkPermission("part", "view")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.globe),
@ -55,7 +55,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
);
}
if (InvenTreeAPI().checkPermission('part', 'change')) {
if (InvenTreeAPI().checkPermission("part", "change")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.edit),
@ -89,9 +89,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
await part.getTestTemplates();
}
void _toggleStar() async {
Future <void> _toggleStar() async {
if (InvenTreeAPI().checkPermission('part', 'view')) {
if (InvenTreeAPI().checkPermission("part", "view")) {
await part.update(values: {"starred": "${!part.starred}"});
refresh();
}
@ -327,7 +327,8 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
}
// TODO - Add request tests?
if (false && part.isTrackable) {
/*
if (part.isTrackable) {
tiles.add(ListTile(
title: Text(L10().testsRequired),
leading: FaIcon(FontAwesomeIcons.tasks),
@ -336,6 +337,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
)
);
}
*/
// Notes field
tiles.add(
@ -398,6 +400,12 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
fields["part"]["hidden"] = true;
int? default_location = part.defaultLocation;
if (default_location != null) {
fields["location"]["value"] = default_location;
}
InvenTreeStockItem().createForm(
context,
L10().stockItemCreate,
@ -405,7 +413,10 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
data: {
"part": "${part.pk}",
},
onSuccess: (data) async {
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data);
@ -437,20 +448,22 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
);
// TODO - Add this action back in once implemented
if (false) {
tiles.add(
ListTile(
title: Text(L10().barcodeScanItem),
leading: FaIcon(FontAwesomeIcons.box),
trailing: FaIcon(FontAwesomeIcons.qrcode),
onTap: () {
// TODO
},
),
);
}
if (false && !part.isActive && InvenTreeAPI().checkPermission('part', 'delete')) {
/*
tiles.add(
ListTile(
title: Text(L10().barcodeScanItem),
leading: FaIcon(FontAwesomeIcons.box),
trailing: FaIcon(FontAwesomeIcons.qrcode),
onTap: () {
// TODO
},
),
);
*/
/*
// TODO: Implement part deletion
if (!part.isActive && InvenTreeAPI().checkPermission("part", "delete")) {
tiles.add(
ListTile(
title: Text(L10().deletePart),
@ -462,6 +475,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
)
);
}
*/
return tiles;
}
@ -480,7 +494,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
),
);
case 1:
return PaginatedStockList({"part": "${part.pk}"});
return PaginatedStockItemList(
{"part": "${part.pk}"}
);
case 2:
return Center(
child: ListView(

View File

@ -1,23 +1,21 @@
import 'dart:io';
import "dart:io";
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import 'package:inventree/api.dart';
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/widget/fields.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:inventree/widget/snacks.dart';
import '../l10.dart';
import "package:inventree/api.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/fields.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/l10.dart";
class PartImageWidget extends StatefulWidget {
PartImageWidget(this.part, {Key? key}) : super(key: key);
const PartImageWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part;
@ -46,7 +44,7 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('part', 'change')) {
if (InvenTreeAPI().checkPermission("part", "change")) {
// File upload
actions.add(

100
lib/widget/part_list.dart Normal file
View File

@ -0,0 +1,100 @@
import "package:flutter/material.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/api.dart";
import "package:inventree/app_settings.dart";
import "package:inventree/l10.dart";
class PartList extends StatefulWidget {
const PartList(this.filters);
final Map<String, String> filters;
@override
_PartListState createState() => _PartListState(filters);
}
class _PartListState extends RefreshableState<PartList> {
_PartListState(this.filters);
final Map<String, String> filters;
@override
String getAppBarTitle(BuildContext context) => L10().parts;
@override
Widget getBody(BuildContext context) {
return PaginatedPartList(filters);
}
}
class PaginatedPartList extends StatefulWidget {
const PaginatedPartList(this.filters, {this.onTotalChanged});
final Map<String, String> filters;
final Function(int)? onTotalChanged;
@override
_PaginatedPartListState createState() => _PaginatedPartListState(filters, onTotalChanged);
}
class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
_PaginatedPartListState(Map<String, String> filters, this.onTotalChanged) : super(filters);
Function(int)? onTotalChanged;
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final bool cascade = await InvenTreeSettingsManager().getBool("partSubcategory", true);
params["cascade"] = "${cascade}";
final page = await InvenTreePart().listPaginated(limit, offset, filters: params);
return page;
}
void _openPart(BuildContext context, int pk) {
// Attempt to load the part information
InvenTreePart().get(pk).then((var part) {
if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
}
});
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreePart part = model as InvenTreePart;
return ListTile(
title: Text(part.fullname),
subtitle: Text("${part.description}"),
trailing: Text("${part.inStockString}"),
leading: InvenTreeAPI().getImage(
part.thumbnail,
width: 40,
height: 40,
),
onTap: () {
_openPart(context, part.pk);
},
);
}
}

View File

@ -1,18 +1,18 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/api.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:inventree/l10.dart';
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:flutter/cupertino.dart";
import "package:flutter_markdown/flutter_markdown.dart";
import "package:inventree/l10.dart";
class PartNotesWidget extends StatefulWidget {
final InvenTreePart part;
const PartNotesWidget(this.part, {Key? key}) : super(key: key);
PartNotesWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part;
@override
_PartNotesState createState() => _PartNotesState(part);
@ -21,10 +21,10 @@ class PartNotesWidget extends StatefulWidget {
class _PartNotesState extends RefreshableState<PartNotesWidget> {
final InvenTreePart part;
_PartNotesState(this.part);
final InvenTreePart part;
@override
Future<void> request() async {
await part.reload();
@ -38,7 +38,7 @@ class _PartNotesState extends RefreshableState<PartNotesWidget> {
List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('part', 'change')) {
if (InvenTreeAPI().checkPermission("part", "change")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.edit),

View File

@ -1,19 +1,19 @@
import 'package:inventree/l10.dart';
import "dart:core";
import 'package:inventree/api.dart';
import "package:inventree/l10.dart";
import 'dart:core';
import "package:inventree/api.dart";
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/inventree/company.dart';
import 'package:inventree/widget/company_detail.dart';
import 'package:inventree/widget/refreshable_state.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/widget/company_detail.dart";
import "package:inventree/widget/refreshable_state.dart";
class PartSupplierWidget extends StatefulWidget {
PartSupplierWidget(this.part, {Key? key}) : super(key: key);
const PartSupplierWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part;

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import "package:flutter/material.dart";
/*
* Construct a circular progress indicator

View File

@ -0,0 +1,384 @@
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:one_context/one_context.dart";
import "package:inventree/api.dart";
import "package:inventree/api_form.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/helpers.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/company_detail.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock_list.dart";
class PurchaseOrderDetailWidget extends StatefulWidget {
const PurchaseOrderDetailWidget(this.order, {Key? key}): super(key: key);
final InvenTreePurchaseOrder order;
@override
_PurchaseOrderDetailState createState() => _PurchaseOrderDetailState(order);
}
class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidget> {
_PurchaseOrderDetailState(this.order);
final InvenTreePurchaseOrder order;
List<InvenTreePOLineItem> lines = [];
int completedLines = 0;
@override
String getAppBarTitle(BuildContext context) => L10().purchaseOrder;
@override
List<Widget> getAppBarActions(BuildContext context) {
List<Widget> actions = [];
if (InvenTreeAPI().checkPermission("purchase_order", "change")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.edit),
tooltip: L10().edit,
onPressed: () {
editOrder(context);
}
)
);
}
return actions;
}
@override
Future<void> request() async {
await order.reload();
lines = await order.getLineItems();
completedLines = 0;
for (var line in lines) {
if (line.isComplete) {
completedLines += 1;
}
}
}
Future <void> editOrder(BuildContext context) async {
order.editForm(
context,
L10().purchaseOrderEdit,
onSuccess: (data) async {
refresh();
}
);
}
Widget headerTile(BuildContext context) {
InvenTreeCompany? supplier = order.supplier;
return Card(
child: ListTile(
title: Text(order.reference),
subtitle: Text(order.description),
leading: supplier == null ? null : InvenTreeAPI().getImage(supplier.thumbnail, width: 40, height: 40),
)
);
}
List<Widget> orderTiles(BuildContext context) {
List<Widget> tiles = [];
InvenTreeCompany? supplier = order.supplier;
tiles.add(headerTile(context));
if (supplier != null) {
tiles.add(ListTile(
title: Text(L10().supplier),
subtitle: Text(supplier.name),
leading: FaIcon(FontAwesomeIcons.building, color: COLOR_CLICK),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyDetailWidget(supplier)
)
);
},
));
}
if (order.supplierReference.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().supplierReference),
subtitle: Text(order.supplierReference),
leading: FaIcon(FontAwesomeIcons.hashtag),
));
}
tiles.add(ListTile(
title: Text(L10().lineItems),
leading: FaIcon(FontAwesomeIcons.clipboardList, color: COLOR_CLICK),
trailing: Text("${order.lineItemCount}"),
onTap: () {
setState(() {
// Switch to the "line items" tab
tabIndex = 1;
});
},
));
tiles.add(ListTile(
title: Text(L10().received),
leading: FaIcon(FontAwesomeIcons.clipboardCheck, color: COLOR_CLICK),
trailing: Text("${completedLines}"),
onTap: () {
setState(() {
// Switch to the "received items" tab
tabIndex = 2;
});
},
));
if (order.issueDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().issueDate),
subtitle: Text(order.issueDate),
leading: FaIcon(FontAwesomeIcons.calendarAlt),
));
}
if (order.targetDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().targetDate),
subtitle: Text(order.targetDate),
leading: FaIcon(FontAwesomeIcons.calendarAlt),
));
}
return tiles;
}
void receiveLine(BuildContext context, InvenTreePOLineItem lineItem) {
Map<String, dynamic> fields = {
"line_item": {
"parent": "items",
"nested": true,
"hidden": true,
"value": lineItem.pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": lineItem.outstanding,
},
"status": {
"parent": "items",
"nested": true,
},
"location": {
},
"barcode": {
"parent": "items",
"nested": true,
"type": "barcode",
"label": L10().barcodeAssign,
"required": false,
}
};
// TODO: Pre-fill the "location" value if the part has a default location specified
launchApiForm(
context,
L10().receiveItem,
order.receive_url,
fields,
method: "POST",
icon: FontAwesomeIcons.signInAlt,
onSuccess: (data) async {
showSnackIcon(L10().receivedItem, success: true);
refresh();
}
);
}
void lineItemMenu(BuildContext context, InvenTreePOLineItem lineItem) {
List<Widget> children = [];
children.add(
SimpleDialogOption(
onPressed: () {
OneContext().popDialog();
// TODO: Navigate to the "SupplierPart" display?
},
child: ListTile(
title: Text(L10().viewSupplierPart),
leading: FaIcon(FontAwesomeIcons.eye),
)
)
);
if (order.isPlaced && InvenTreeAPI().supportPoReceive()) {
children.add(
SimpleDialogOption(
onPressed: () {
// Hide the dialog option
OneContext().popDialog();
receiveLine(context, lineItem);
},
child: ListTile(
title: Text(L10().receiveItem),
leading: FaIcon(FontAwesomeIcons.signInAlt),
)
)
);
}
// No valid actions available
if (children.isEmpty) {
return;
}
children.insert(0, Divider());
showDialog(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: Text(L10().lineItem),
children: children,
);
}
);
}
List<Widget> lineTiles(BuildContext context) {
List<Widget> tiles = [];
tiles.add(headerTile(context));
for (var line in lines) {
InvenTreeSupplierPart? supplierPart = line.supplierPart;
if (supplierPart != null) {
String q = simpleNumberString(line.quantity);
Color c = Colors.black;
if (order.isOpen) {
q = simpleNumberString(line.received) + " / " + simpleNumberString(line.quantity);
if (line.isComplete) {
c = COLOR_SUCCESS;
} else {
c = COLOR_DANGER;
}
}
tiles.add(
ListTile(
title: Text(supplierPart.SKU),
subtitle: Text(supplierPart.partName),
leading: InvenTreeAPI().getImage(supplierPart.partImage, width: 40, height: 40),
trailing: Text(
q,
style: TextStyle(
color: c,
),
),
onTap: () {
// TODO: ?
},
onLongPress: () {
lineItemMenu(context, line);
},
)
);
}
}
return tiles;
}
@override
Widget getBody(BuildContext context) {
return Center(
child: getSelectedWidget(context, tabIndex),
);
}
Widget getSelectedWidget(BuildContext context, int index) {
switch (index) {
case 0:
return ListView(
children: orderTiles(context)
);
case 1:
return ListView(
children: lineTiles(context)
);
case 2:
// Stock items received against this order
Map<String, String> filters = {
"purchase_order": "${order.pk}"
};
return PaginatedStockItemList(filters);
default:
return ListView();
}
}
@override
Widget getBottomNavBar(BuildContext context) {
return BottomNavigationBar(
currentIndex: tabIndex,
onTap: onTabSelectionChanged,
items: [
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.info),
label: L10().details
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.thList),
label: L10().lineItems,
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.boxes),
label: L10().stockItems
)
],
);
}
}

View File

@ -0,0 +1,96 @@
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/purchase_order_detail.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
import "package:inventree/api.dart";
import "package:inventree/inventree/purchase_order.dart";
/*
* Widget class for displaying a list of Purchase Orders
*/
class PurchaseOrderListWidget extends StatefulWidget {
const PurchaseOrderListWidget({this.filters = const {}, Key? key}) : super(key: key);
final Map<String, String> filters;
@override
_PurchaseOrderListWidgetState createState() => _PurchaseOrderListWidgetState(filters);
}
class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWidget> {
_PurchaseOrderListWidgetState(this.filters);
final Map<String, String> filters;
@override
String getAppBarTitle(BuildContext context) => L10().purchaseOrders;
@override
Widget getBody(BuildContext context) {
return PaginatedPurchaseOrderList(filters);
}
}
class PaginatedPurchaseOrderList extends StatefulWidget {
const PaginatedPurchaseOrderList(this.filters);
final Map<String, String> filters;
@override
_PaginatedPurchaseOrderListState createState() => _PaginatedPurchaseOrderListState(filters);
}
class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPurchaseOrderList> {
_PaginatedPurchaseOrderListState(Map<String, String> filters) : super(filters);
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
params["outstanding"] = "true";
final page = await InvenTreePurchaseOrder().listPaginated(limit, offset, filters: params);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreePurchaseOrder order = model as InvenTreePurchaseOrder;
InvenTreeCompany? supplier = order.supplier;
return ListTile(
title: Text(order.reference),
subtitle: Text(order.description),
leading: supplier == null ? null : InvenTreeAPI().getImage(
supplier.thumbnail,
width: 40,
height: 40,
),
trailing: Text("${order.lineItemCount}"),
onTap: () async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderDetailWidget(order)
)
);
},
);
}
}

View File

@ -1,7 +1,8 @@
import 'package:inventree/widget/drawer.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import "package:inventree/widget/back.dart";
import "package:inventree/widget/drawer.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter/widgets.dart";
abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
@ -9,7 +10,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
final refreshableKey = GlobalKey<ScaffoldState>();
// Storage for context once "Build" is called
BuildContext? _context;
late BuildContext? _context;
// Current tab index (used for widgets which display bottom tabs)
int tabIndex = 0;
@ -32,6 +33,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
String getAppBarTitle(BuildContext context) { return "App Bar Title"; }
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) => onBuild(_context!));
@ -60,14 +62,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
});
}
// Function to construct an appbar (override if needed)
AppBar getAppBar(BuildContext context) {
return AppBar(
title: Text(getAppBarTitle(context)),
actions: getAppBarActions(context),
);
}
// Function to construct a drawer (override if needed)
Widget getDrawer(BuildContext context) {
return InvenTreeDrawer(context);
@ -96,8 +90,12 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
return Scaffold(
key: refreshableKey,
appBar: getAppBar(context),
drawer: null,
appBar: AppBar(
title: Text(getAppBarTitle(context)),
actions: getAppBarActions(context),
leading: backButton(context, refreshableKey),
),
drawer: getDrawer(context),
floatingActionButton: getFab(context),
body: Builder(
builder: (BuildContext context) {

View File

@ -1,393 +1,347 @@
import "dart:async";
import 'package:inventree/widget/part_detail.dart';
import 'package:inventree/widget/progress.dart';
import 'package:inventree/widget/snacks.dart';
import 'package:inventree/widget/stock_detail.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/l10.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import 'package:inventree/inventree/part.dart';
import 'package:inventree/inventree/stock.dart';
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import '../api.dart';
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/part_list.dart";
import "package:inventree/widget/purchase_order_list.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/stock_list.dart";
import "package:inventree/widget/category_list.dart";
import "package:inventree/widget/company_list.dart";
import "package:inventree/widget/location_list.dart";
// TODO - Refactor duplicate code in this file!
class PartSearchDelegate extends SearchDelegate<InvenTreePart?> {
final partSearchKey = GlobalKey<ScaffoldState>();
BuildContext context;
// What did we search for last time?
String _cachedQuery = "";
bool _searching = false;
// Custom filters for the part search
Map<String, String> _filters = {};
PartSearchDelegate(this.context, {Map<String, String> filters = const {}}) {
// Copy filter values
for (String key in filters.keys) {
String? value = filters[key];
if (value != null) {
_filters[key] = value;
}
}
}
// Widget for performing database-wide search
class SearchWidget extends StatefulWidget {
@override
String get searchFieldLabel => L10().searchParts;
_SearchDisplayState createState() => _SearchDisplayState();
// List of part results
List<InvenTreePart> partResults = [];
Future<void> search(BuildContext context) async {
// Search string too short!
if (query.length < 3) {
partResults.clear();
showResults(context);
return;
}
if (query == _cachedQuery) {
return;
}
_cachedQuery = query;
_searching = true;
print("Searching...");
showResults(context);
_filters["cascade"] = "true";
final results = await InvenTreePart().search(context, query, filters: _filters);
partResults.clear();
for (int idx = 0; idx < results.length; idx++) {
if (results[idx] is InvenTreePart) {
partResults.add(results[idx] as InvenTreePart);
}
}
print("Searching complete! Results: ${partResults.length}");
_searching = false;
showSnackIcon(
"${partResults.length} ${L10().results}",
success: partResults.length > 0,
icon: FontAwesomeIcons.pollH,
);
// For some reason, need to toggle between suggestions and results here...
showSuggestions(context);
showResults(context);
}
@override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: FaIcon(FontAwesomeIcons.backspace),
onPressed: () {
query = '';
search(context);
},
),
IconButton(
icon: FaIcon(FontAwesomeIcons.search),
onPressed: () {
search(context);
}
),
];
}
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
this.close(context, null);
}
);
}
Widget _partResult(BuildContext context, int index) {
InvenTreePart part = partResults[index];
return ListTile(
title: Text(part.fullname),
subtitle: Text(part.description),
leading: InvenTreeAPI().getImage(
part.thumbnail,
width: 40,
height: 40
),
trailing: Text(part.inStockString),
onTap: () {
InvenTreePart().get(part.pk).then((var prt) {
if (prt is InvenTreePart) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => PartDetailWidget(prt))
);
}
});
}
);
}
@override
Widget buildResults(BuildContext context) {
print("build results");
if (_searching) {
return progressIndicator();
}
search(context);
if (query.length == 0) {
return ListTile(
title: Text(L10().queryEnter)
);
}
if (query.length < 3) {
return ListTile(
title: Text(L10().queryShort),
subtitle: Text(L10().queryShortDetail)
);
}
if (partResults.length == 0) {
return ListTile(
title: Text(L10().noResults),
subtitle: Text(L10().queryNoResults + " '${query}'")
);
}
return ListView.separated(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
separatorBuilder: (_, __) => const Divider(height: 3),
itemBuilder: _partResult,
itemCount: partResults.length,
);
}
@override
Widget buildSuggestions(BuildContext context) {
// TODO - Implement
return Column();
}
// Ensure the search theme matches the app theme
@override
ThemeData appBarTheme(BuildContext context) {
final ThemeData theme = Theme.of(context);
return theme;
}
}
class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem?> {
final stockSearchKey = GlobalKey<ScaffoldState>();
final BuildContext context;
String _cachedQuery = "";
bool _searching = false;
// Custom filters for the stock item search
Map<String, String> _filters = {};
StockSearchDelegate(this.context, {Map<String, String> filters = const {}}) {
// Copy filter values
for (String key in filters.keys) {
String? value = filters[key];
if (value != null) {
_filters[key] = value;
}
}
}
class _SearchDisplayState extends RefreshableState<SearchWidget> {
@override
String get searchFieldLabel => L10().searchStock;
String getAppBarTitle(BuildContext context) => L10().search;
// List of StockItem results
List<InvenTreeStockItem> itemResults = [];
final TextEditingController searchController = TextEditingController();
Timer? debounceTimer;
int nPartResults = 0;
int nCategoryResults = 0;
int nStockResults = 0;
int nLocationResults = 0;
int nSupplierResults = 0;
int nPurchaseOrderResults = 0;
// Callback when the text is being edited
// Incorporates a debounce timer to restrict search frequency
void onSearchTextChanged(String text, {bool immediate = false}) {
if (debounceTimer?.isActive ?? false) {
debounceTimer!.cancel();
}
if (immediate) {
search(text);
} else {
debounceTimer = Timer(Duration(milliseconds: 250), () {
search(text);
});
}
}
Future<void> search(String term) async {
if (term.isEmpty) {
setState(() {
// Do not search on an empty string
nPartResults = 0;
nCategoryResults = 0;
nStockResults = 0;
nLocationResults = 0;
nSupplierResults = 0;
nPurchaseOrderResults = 0;
});
Future<void> search(BuildContext context) async {
// Search string too short!
if (query.length < 3) {
itemResults.clear();
showResults(context);
return;
}
if (query == _cachedQuery) {
return;
}
// Search parts
InvenTreePart().count(
searchQuery: term
).then((int n) {
setState(() {
nPartResults = n;
});
});
_cachedQuery = query;
// Search part categories
InvenTreePartCategory().count(
searchQuery: term,
).then((int n) {
setState(() {
nCategoryResults = n;
});
});
_searching = true;
// Search stock items
InvenTreeStockItem().count(
searchQuery: term
).then((int n) {
setState(() {
nStockResults = n;
});
});
print("Searching...");
// Search stock locations
InvenTreeStockLocation().count(
searchQuery: term
).then((int n) {
setState(() {
nLocationResults = n;
});
});
showResults(context);
// Search suppliers
InvenTreeCompany().count(
searchQuery: term,
filters: {
"is_supplier": "true",
},
).then((int n) {
setState(() {
nSupplierResults = n;
});
});
// Enable cascading part search by default
_filters["cascade"] = "true";
final results = await InvenTreeStockItem().search(
context, query, filters: _filters);
itemResults.clear();
for (int idx = 0; idx < results.length; idx++) {
if (results[idx] is InvenTreeStockItem) {
itemResults.add(results[idx] as InvenTreeStockItem);
// Search purchase orders
InvenTreePurchaseOrder().count(
searchQuery: term,
filters: {
"outstanding": "true"
}
}
).then((int n) {
setState(() {
nPurchaseOrderResults = n;
});
});
_searching = false;
}
showSnackIcon(
"${itemResults.length} ${L10().results}",
success: itemResults.length > 0,
icon: FontAwesomeIcons.pollH,
List<Widget> _tiles(BuildContext context) {
List<Widget> tiles = [];
// Search input
tiles.add(
InputDecorator(
decoration: InputDecoration(
),
child: ListTile(
title: TextField(
readOnly: false,
controller: searchController,
onChanged: (String text) {
onSearchTextChanged(text);
},
),
leading: IconButton(
icon: FaIcon(FontAwesomeIcons.backspace, color: Colors.red),
onPressed: () {
searchController.clear();
onSearchTextChanged("", immediate: true);
},
),
)
)
);
showSuggestions(context);
showResults(context);
}
String query = searchController.text;
@override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: FaIcon(FontAwesomeIcons.backspace),
onPressed: () {
query = '';
search(context);
},
),
IconButton(
icon: FaIcon(FontAwesomeIcons.search),
onPressed: () {
search(context);
}
),
];
}
List<Widget> results = [];
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
this.close(context, null);
}
);
}
Widget _itemResult(BuildContext context, int index) {
InvenTreeStockItem item = itemResults[index];
return ListTile(
title: Text(item.partName),
subtitle: Text(item.locationName),
leading: InvenTreeAPI().getImage(
item.partThumbnail,
width: 40,
height: 40,
),
trailing: Text(item.serialOrQuantityDisplay()),
onTap: () {
InvenTreeStockItem().get(item.pk).then((var it) {
if (it is InvenTreeStockItem) {
// Part Results
if (nPartResults > 0) {
results.add(
ListTile(
title: Text(L10().parts),
leading: FaIcon(FontAwesomeIcons.shapes),
trailing: Text("${nPartResults}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => StockDetailWidget(it))
context,
MaterialPageRoute(
builder: (context) => PartList(
{
"original_search": query
}
)
)
);
}
});
)
);
}
// Part Category Results
if (nCategoryResults > 0) {
results.add(
ListTile(
title: Text(L10().partCategories),
leading: FaIcon(FontAwesomeIcons.sitemap),
trailing: Text("${nCategoryResults}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartCategoryList(
{
"original_search": query
}
)
)
);
},
)
);
}
// Stock Item Results
if (nStockResults > 0) {
results.add(
ListTile(
title: Text(L10().stockItems),
leading: FaIcon(FontAwesomeIcons.boxes),
trailing: Text("${nStockResults}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockItemList(
{
"original_search": query,
}
)
)
);
},
)
);
}
// Stock location results
if (nLocationResults > 0) {
results.add(
ListTile(
title: Text(L10().stockLocations),
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
trailing: Text("${nLocationResults}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockLocationList(
{
"original_search": query
}
)
)
);
},
)
);
}
// Suppliers
if (nSupplierResults > 0) {
results.add(
ListTile(
title: Text(L10().suppliers),
leading: FaIcon(FontAwesomeIcons.building),
trailing: Text("${nSupplierResults}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyListWidget(
L10().suppliers,
{
"is_supplier": "true",
"original_search": query
}
)
)
);
},
)
);
}
// Purchase orders
if (nPurchaseOrderResults > 0) {
results.add(
ListTile(
title: Text(L10().purchaseOrders),
leading: FaIcon(FontAwesomeIcons.shoppingCart),
trailing: Text("${nPurchaseOrderResults}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderListWidget(
filters: {
"original_search": query
}
)
)
);
},
)
);
}
if (results.isEmpty) {
tiles.add(
ListTile(
title: Text(L10().queryNoResults),
leading: FaIcon(FontAwesomeIcons.search),
)
);
} else {
for (Widget result in results) {
tiles.add(result);
}
}
return tiles;
}
@override
Widget getBody(BuildContext context) {
return Center(
child: ListView(
children: ListTile.divideTiles(
context: context,
tiles: _tiles(context),
).toList()
)
);
}
@override
Widget buildResults(BuildContext context) {
search(context);
if (_searching) {
return progressIndicator();
}
search(context);
if (query.length == 0) {
return ListTile(
title: Text(L10().queryEnter)
);
}
if (query.length < 3) {
return ListTile(
title: Text(L10().queryShort),
subtitle: Text(L10().queryShortDetail)
);
}
if (itemResults.length == 0) {
return ListTile(
title: Text(L10().noResults),
subtitle: Text(L10().queryNoResults + " '${query}'")
);
}
return ListView.separated(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
separatorBuilder: (_, __) => const Divider(height: 3),
itemBuilder: _itemResult,
itemCount: itemResults.length,
);
}
@override
Widget buildSuggestions(BuildContext context) {
// TODO - Implement
return Column();
}
// Ensure the search theme matches the app theme
@override
ThemeData appBarTheme(BuildContext context) {
final ThemeData theme = Theme.of(context);
return theme;
}
}
}

View File

@ -8,16 +8,20 @@
* | Text <icon> |
*/
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:one_context/one_context.dart';
import 'package:inventree/l10.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:one_context/one_context.dart";
import "package:inventree/l10.dart";
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
OneContext().hideCurrentSnackBar();
BuildContext? context = OneContext().context;
if (context != null) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
}
Color backgroundColor = Colors.deepOrange;

View File

@ -1,13 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import "package:flutter/material.dart";
import "package:flutter/cupertino.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/app_colors.dart';
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/app_colors.dart";
class Spinner extends StatefulWidget {
final IconData? icon;
final Duration duration;
final Color color;
const Spinner({
this.color = COLOR_GRAY_LIGHT,
@ -16,12 +13,16 @@ class Spinner extends StatefulWidget {
this.duration = const Duration(milliseconds: 1800),
}) : super(key: key);
final IconData? icon;
final Duration duration;
final Color color;
@override
_SpinnerState createState() => _SpinnerState();
}
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
AnimationController? _controller;
late AnimationController? _controller;
Widget? _child;
@override

View File

@ -1,20 +1,18 @@
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:inventree/l10.dart";
import 'package:inventree/inventree/part.dart';
import 'package:inventree/widget/part_detail.dart';
import 'package:inventree/widget/progress.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:inventree/l10.dart';
import '../api.dart';
import "package:inventree/api.dart";
class StarredPartWidget extends StatefulWidget {
StarredPartWidget({Key? key}) : super(key: key);
const StarredPartWidget({Key? key}) : super(key: key);
@override
_StarredPartState createState() => _StarredPartState();
@ -73,7 +71,7 @@ class _StarredPartState extends RefreshableState<StarredPartWidget> {
return progressIndicator();
}
if (starredParts.length == 0) {
if (starredParts.isEmpty) {
return ListView(
children: [
ListTile(

View File

@ -1,30 +1,30 @@
import 'package:inventree/app_colors.dart';
import 'package:inventree/barcode.dart';
import 'package:inventree/inventree/model.dart';
import 'package:inventree/inventree/stock.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/widget/dialogs.dart';
import 'package:inventree/widget/fields.dart';
import 'package:inventree/widget/location_display.dart';
import 'package:inventree/widget/part_detail.dart';
import 'package:inventree/widget/progress.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:inventree/widget/snacks.dart';
import 'package:inventree/widget/stock_item_test_results.dart';
import 'package:inventree/widget/stock_notes.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import "package:inventree/app_colors.dart";
import "package:inventree/barcode.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/fields.dart";
import "package:inventree/widget/location_display.dart";
import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock_item_test_results.dart";
import "package:inventree/widget/stock_notes.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import 'package:inventree/l10.dart';
import "package:inventree/l10.dart";
import "package:inventree/helpers.dart";
import "package:inventree/api.dart";
import 'package:inventree/api.dart';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import "package:dropdown_search/dropdown_search.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
class StockDetailWidget extends StatefulWidget {
StockDetailWidget(this.item, {Key? key}) : super(key: key);
const StockDetailWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item;
@ -35,6 +35,8 @@ class StockDetailWidget extends StatefulWidget {
class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
_StockItemDisplayState(this.item);
@override
String getAppBarTitle(BuildContext context) => L10().stockItem;
@ -46,14 +48,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
final _countStockKey = GlobalKey<FormState>();
final _moveStockKey = GlobalKey<FormState>();
_StockItemDisplayState(this.item);
@override
List<Widget> getAppBarActions(BuildContext context) {
List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('stock', 'view')) {
if (InvenTreeAPI().checkPermission("stock", "view")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.globe),
@ -62,7 +62,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
);
}
if (InvenTreeAPI().checkPermission('stock', 'change')) {
if (InvenTreeAPI().checkPermission("stock", "change")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.edit),
@ -99,13 +99,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
await item.reload();
// Request part information
part = await InvenTreePart().get(item.partId) as InvenTreePart;
part = await InvenTreePart().get(item.partId) as InvenTreePart?;
// Request test results...
await item.getTestResults();
}
void _editStockItem(BuildContext context) async {
Future <void> _editStockItem(BuildContext context) async {
var fields = InvenTreeStockItem().formFields();
@ -125,7 +125,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
void _addStock() async {
Future <void> _addStock() async {
double quantity = double.parse(_quantityController.text);
_quantityController.clear();
@ -138,7 +138,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
refresh();
}
void _addStockDialog() async {
Future <void> _addStockDialog() async {
_quantityController.clear();
_notesController.clear();
@ -171,7 +171,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
}
void _removeStock() async {
Future <void> _removeStock() async {
double quantity = double.parse(_quantityController.text);
_quantityController.clear();
@ -211,7 +211,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
);
}
void _countStock() async {
Future <void> _countStock() async {
double quantity = double.parse(_quantityController.text);
_quantityController.clear();
@ -223,9 +223,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
refresh();
}
void _countStockDialog() async {
Future <void> _countStockDialog() async {
_quantityController.text = item.quantityString;
_quantityController.text = item.quantity.toString();
_notesController.clear();
showFormDialog(L10().countStock,
@ -251,9 +251,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
void _unassignBarcode(BuildContext context) async {
Future<void> _unassignBarcode(BuildContext context) async {
final bool result = await item.update(values: {'uid': ''});
final bool result = await item.update(values: {"uid": ""});
if (result) {
showSnackIcon(
@ -271,7 +271,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
void _transferStock(int locationId) async {
Future <void> _transferStock(int locationId) async {
double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
String notes = _notesController.text;
@ -288,11 +288,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
}
void _transferStockDialog(BuildContext context) async {
Future <void> _transferStockDialog(BuildContext context) async {
int? location_pk;
_quantityController.text = "${item.quantityString}";
_quantityController.text = "${item.quantity}";
showFormDialog(L10().transferStock,
key: _moveStockKey,
@ -327,13 +327,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
},
onFind: (String filter) async {
Map<String, String> _filters = {
"search": filter,
"offset": "0",
"limit": "25"
};
final List<InvenTreeModel> results = await InvenTreeStockLocation().list(filters: _filters);
final results = await InvenTreeStockLocation().search(filter);
List<dynamic> items = [];
@ -349,13 +343,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
hint: L10().searchLocation,
onChanged: null,
itemAsString: (dynamic location) {
return location['pathstring'];
return (location["pathstring"] ?? "") as String;
},
onSaved: (dynamic location) {
if (location == null) {
location_pk = null;
} else {
location_pk = location['pk'];
location_pk = location["pk"] as int;
}
},
isFilteredOnline: true,
@ -420,7 +414,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
ListTile(
title: Text(L10().quantity),
leading: FaIcon(FontAwesomeIcons.cubes),
trailing: Text("${item.quantityString}"),
trailing: Text("${item.quantityString()}"),
)
);
}
@ -503,7 +497,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
// Supplier part?
// TODO: Display supplier part info page?
if (false && item.supplierPartId > 0) {
/*
if (item.supplierPartId > 0) {
tiles.add(
ListTile(
title: Text("${item.supplierName}"),
@ -514,6 +509,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
)
);
}
*/
if (item.link.isNotEmpty) {
tiles.add(
@ -559,7 +555,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
// TODO - Is this stock item linked to a PurchaseOrder?
// TODO - Re-enable stock item history display
if (false && item.trackingItemCount > 0) {
/*
if (item.trackingItemCount > 0) {
tiles.add(
ListTile(
title: Text(L10().history),
@ -574,6 +571,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
)
);
}
*/
// Notes field
tiles.add(
@ -600,7 +598,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
tiles.add(headerTile());
// First check that the user has the required permissions to adjust stock
if (!InvenTreeAPI().checkPermission('stock', 'change')) {
if (!InvenTreeAPI().checkPermission("stock", "change")) {
tiles.add(
ListTile(
title: Text(L10().permissionRequired),
@ -624,7 +622,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
title: Text(L10().countStock),
leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK),
onTap: _countStockDialog,
trailing: Text(item.quantityString),
trailing: Text(item.quantityString(includeUnits: true)),
)
);
@ -678,12 +676,31 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
trailing: FaIcon(FontAwesomeIcons.qrcode),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemBarcodeAssignmentHandler(item)))
).then((context) {
refresh();
var handler = UniqueBarcodeHandler((String hash) {
item.update(
values: {
"uid": hash,
}
).then((result) {
if (result) {
successTone();
showSnackIcon(
L10().barcodeAssigned,
success: true,
icon: FontAwesomeIcons.qrcode
);
refresh();
}
});
});
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvenTreeQRView(handler))
);
}
)
);
@ -710,12 +727,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
items: <BottomNavigationBarItem> [
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.infoCircle),
title: Text(L10().details),
label: L10().details,
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.wrench),
title: Text(L10().actions),
),
label: L10().actions, ),
]
);
}

View File

@ -1,27 +1,21 @@
import 'package:inventree/api_form.dart';
import 'package:inventree/app_colors.dart';
import 'package:inventree/inventree/part.dart';
import 'package:inventree/inventree/stock.dart';
import 'package:inventree/inventree/model.dart';
import 'package:inventree/api.dart';
import 'package:inventree/widget/dialogs.dart';
import 'package:inventree/widget/fields.dart';
import 'package:inventree/widget/progress.dart';
import 'package:inventree/widget/snacks.dart';
import "package:inventree/app_colors.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/api.dart";
import "package:inventree/widget/progress.dart";
import 'package:inventree/l10.dart';
import "package:inventree/l10.dart";
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
class StockItemTestResultsWidget extends StatefulWidget {
StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
const StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item;
@ -32,7 +26,7 @@ class StockItemTestResultsWidget extends StatefulWidget {
class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestResultsWidget> {
final _addResultKey = GlobalKey<FormState>();
_StockItemTestResultDisplayState(this.item);
@override
String getAppBarTitle(BuildContext context) => L10().testResults;
@ -57,9 +51,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
final InvenTreeStockItem item;
_StockItemTestResultDisplayState(this.item);
void addTestResult(BuildContext context, {String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
Future <void> addTestResult(BuildContext context, {String name = "", bool nameIsEditable = true, bool result = false, String value = "", bool valueRequired = false, bool attachmentRequired = false}) async {
InvenTreeStockItemTestResult().createForm(
context,
@ -150,7 +142,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
var results = getTestResults();
if (results.length == 0) {
if (results.isEmpty) {
tiles.add(ListTile(
title: Text(L10().testResultNone),
subtitle: Text(L10().testResultNoneDetail),
@ -165,7 +157,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
String _test = "";
bool _result = false;
String _value = "";
String _notes = "";
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
bool _valueRequired = false;
@ -175,8 +166,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
_result = item.passFailStatus();
_test = item.testName;
_required = item.required;
_value = item.latestResult()?.value ?? '';
_notes = item.latestResult()?.notes ?? '';
_value = item.latestResult()?.value ?? "";
_valueRequired = item.requiresValue;
_attachmentRequired = item.requiresAttachment;
} else if (item is InvenTreeStockItemTestResult) {
@ -184,7 +174,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
_test = item.testName;
_required = false;
_value = item.value;
_notes = item.notes;
}
if (_result == true) {

105
lib/widget/stock_list.dart Normal file
View File

@ -0,0 +1,105 @@
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
import "package:inventree/app_settings.dart";
import "package:inventree/widget/stock_detail.dart";
import "package:inventree/api.dart";
class StockItemList extends StatefulWidget {
const StockItemList(this.filters);
final Map<String, String> filters;
@override
_StockListState createState() => _StockListState(filters);
}
class _StockListState extends RefreshableState<StockItemList> {
_StockListState(this.filters);
final Map<String, String> filters;
@override
String getAppBarTitle(BuildContext context) => L10().purchaseOrders;
@override
Widget getBody(BuildContext context) {
return PaginatedStockItemList(filters);
}
}
class PaginatedStockItemList extends StatefulWidget {
const PaginatedStockItemList(this.filters);
final Map<String, String> filters;
@override
_PaginatedStockItemListState createState() => _PaginatedStockItemListState(filters);
}
class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockItemList> {
_PaginatedStockItemListState(Map<String, String> filters) : super(filters);
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
// Do we include stock items from sub-locations?
final bool cascade = await InvenTreeSettingsManager().getBool("stockSublocation", true);
params["cascade"] = "${cascade}";
final page = await InvenTreeStockItem().listPaginated(
limit,
offset,
filters: params
);
return page;
}
void _openItem(BuildContext context, int pk) {
InvenTreeStockItem().get(pk).then((var item) {
if (item is InvenTreeStockItem) {
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
}
});
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeStockItem item = model as InvenTreeStockItem;
return ListTile(
title: Text("${item.partName}"),
subtitle: Text("${item.locationPathString}"),
leading: InvenTreeAPI().getImage(
item.partThumbnail,
width: 40,
height: 40,
),
trailing: Text("${item.displayQuantity}",
style: TextStyle(
fontWeight: FontWeight.bold,
color: item.statusColor,
),
),
onTap: () {
_openItem(context, item.pk);
},
);
}
}

View File

@ -1,20 +1,20 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/inventree/stock.dart';
import 'package:inventree/widget/refreshable_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:inventree/l10.dart';
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:flutter/cupertino.dart";
import "package:flutter_markdown/flutter_markdown.dart";
import "package:inventree/l10.dart";
import '../api.dart';
import "package:inventree/api.dart";
class StockNotesWidget extends StatefulWidget {
final InvenTreeStockItem item;
const StockNotesWidget(this.item, {Key? key}) : super(key: key);
StockNotesWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item;
@override
_StockNotesState createState() => _StockNotesState(item);
@ -23,10 +23,10 @@ class StockNotesWidget extends StatefulWidget {
class _StockNotesState extends RefreshableState<StockNotesWidget> {
final InvenTreeStockItem item;
_StockNotesState(this.item);
final InvenTreeStockItem item;
@override
String getAppBarTitle(BuildContext context) => L10().stockItemNotes;
@ -39,7 +39,7 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> {
List<Widget> getAppBarActions(BuildContext context) {
List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('stock', 'change')) {
if (InvenTreeAPI().checkPermission("stock", "change")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.edit),

View File

@ -1,12 +1,10 @@
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/snacks.dart";
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/inventree/sentry.dart';
import 'package:inventree/widget/snacks.dart';
import '../l10.dart';
import "package:inventree/l10.dart";
class SubmitFeedbackWidget extends StatefulWidget {
@ -18,7 +16,7 @@ class SubmitFeedbackWidget extends StatefulWidget {
class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
final _formkey = new GlobalKey<FormState>();
final _formkey = GlobalKey<FormState>();
String message = "";
@ -61,8 +59,6 @@ class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
key: _formkey,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(

View File

@ -49,7 +49,21 @@ packages:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.1.0"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
camera:
dependency: "direct main"
description:
@ -113,6 +127,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
datetime_picker_formfield:
dependency: "direct main"
description:
name: datetime_picker_formfield
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
device_info_plus:
dependency: "direct main"
description:
@ -315,6 +336,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
lint:
dependency: "direct dev"
description:
name: lint
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
markdown:
dependency: transitive
description:

View File

@ -13,40 +13,43 @@ environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
audioplayers: ^0.20.1 # Play audio files
cached_network_image: ^3.1.0 # Download and cache remote images
camera: # Camera
cupertino_icons: ^1.0.3
datetime_picker_formfield: ^2.0.0 # Date / time picker
device_info_plus: ^2.1.0 # Information about the device
dropdown_search: 0.6.3 # Dropdown autocomplete form fields
file_picker: ^4.0.0 # Select files from the device
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.17.0
cupertino_icons: ^1.0.3
http: ^0.13.0
cached_network_image: ^3.0.0 # Download and cache remote images
qr_code_scanner: ^0.5.2 # Barcode scanning
package_info_plus: ^1.0.4 # App information introspection
device_info_plus: ^2.1.0 # Information about the device
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
sentry_flutter: 5.0.0 # Error reporting
image_picker: ^0.8.3 # Select or take photos
file_picker: ^4.0.0 # Select files from the device
url_launcher: 6.0.9 # Open link in system browser
open_file: 3.2.1 # Open local files
flutter_markdown: ^0.6.2 # Rendering markdown
camera: # Camera
path_provider: 2.0.2 # Local file storage
sembast: ^3.1.0+2 # NoSQL data storage
one_context: ^1.1.0 # Dialogs without requiring context
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
http: ^0.13.0
image_picker: ^0.8.3 # Select or take photos
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
audioplayers: ^0.20.1 # Play audio files
dropdown_search: 0.6.3 # Dropdown autocomplete form fields
intl: ^0.17.0
one_context: ^1.1.0 # Dialogs without requiring context
open_file: 3.2.1 # Open local files
package_info_plus: ^1.0.4 # App information introspection
path:
path_provider: 2.0.2 # Local file storage
qr_code_scanner: ^0.5.2 # Barcode scanning
sembast: ^3.1.0+2 # NoSQL data storage
sentry_flutter: 5.0.0 # Error reporting
url_launcher: 6.0.9 # Open link in system browser
dev_dependencies:
flutter_launcher_icons:
flutter_test:
sdk: flutter
flutter_launcher_icons:
lint: ^1.0.0
flutter_icons:
android: true

View File

@ -5,9 +5,9 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter_test/flutter_test.dart';
import "package:flutter_test/flutter_test.dart";
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
testWidgets("Counter increments smoke test", (WidgetTester tester) async {
});
}