2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-05-03 15:58:54 +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 .history
.svn/ .svn/
coverage/*
# Sentry API key # Sentry API key
lib/dsn.dart 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 { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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 ## 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 ### 0.4.7 - September 2021
--- ---

View File

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

View File

@ -1,26 +1,24 @@
import 'package:inventree/app_settings.dart'; import "dart:io";
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 '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:qr_code_scanner/qr_code_scanner.dart";
import 'package:inventree/inventree/part.dart';
import 'package:inventree/l10.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/location_display.dart";
import 'package:inventree/widget/part_detail.dart'; import "package:inventree/widget/part_detail.dart";
import 'package:inventree/widget/stock_detail.dart'; import "package:inventree/widget/stock_detail.dart";
import 'dart:io';
class BarcodeHandler { class BarcodeHandler {
@ -32,32 +30,12 @@ class BarcodeHandler {
* based on the response returned from the InvenTree server * 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; 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");
}
}
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async { Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// Called when the server "matches" a barcode // Called when the server "matches" a barcode
// Override this function // Override this function
@ -101,8 +79,10 @@ class BarcodeHandler {
_controller?.resumeCamera(); _controller?.resumeCamera();
Map<String, dynamic> data = response.asMap();
// Handle strange response from the server // Handle strange response from the server
if (!response.isValid() || response.data == null || !(response.data is Map)) { if (!response.isValid() || !response.isMap()) {
onBarcodeUnknown(context, {}); onBarcodeUnknown(context, {});
// We want to know about this one! // We want to know about this one!
@ -118,12 +98,12 @@ class BarcodeHandler {
"errorDetail": response.errorDetail, "errorDetail": response.errorDetail,
} }
); );
} else if (response.data.containsKey('error')) { } else if (data.containsKey("error")) {
onBarcodeUnknown(context, response.data); onBarcodeUnknown(context, data);
} else if (response.data.containsKey('success')) { } else if (data.containsKey("success")) {
onBarcodeMatched(context, response.data); onBarcodeMatched(context, data);
} else { } else {
onBarcodeUnhandled(context, response.data); onBarcodeUnhandled(context, data);
} }
} }
} }
@ -156,9 +136,9 @@ class BarcodeScanHandler extends BarcodeHandler {
int pk = -1; int pk = -1;
// A stocklocation has been passed? // 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) { 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) { if (pk > 0) {
@ -206,9 +186,9 @@ class BarcodeScanHandler extends BarcodeHandler {
success: false 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) { 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 { class StockItemScanIntoLocationHandler extends BarcodeHandler {
/* /*
* Barcode handler for scanning a provided StockItem into a scanned StockLocation * Barcode handler for scanning a provided StockItem into a scanned StockLocation
*/ */
final InvenTreeStockItem item;
StockItemScanIntoLocationHandler(this.item); StockItemScanIntoLocationHandler(this.item);
final InvenTreeStockItem item;
@override @override
String getOverlayText(BuildContext context) => L10().barcodeScanLocation; String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
@override @override
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async { Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// If the barcode points to a 'stocklocation', great! // If the barcode points to a "stocklocation", great!
if (data.containsKey('stocklocation')) { if (data.containsKey("stocklocation")) {
// Extract location information // Extract location information
int location = (data['stocklocation']['pk'] ?? -1) as int; int location = (data["stocklocation"]["pk"] ?? -1) as int;
if (location == -1) { if (location == -1) {
showSnackIcon( showSnackIcon(
@ -395,10 +306,10 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
* Barcode handler for scanning stock item(s) into the specified StockLocation * Barcode handler for scanning stock item(s) into the specified StockLocation
*/ */
final InvenTreeStockLocation location;
StockLocationScanInItemsHandler(this.location); StockLocationScanInItemsHandler(this.location);
final InvenTreeStockLocation location;
@override @override
String getOverlayText(BuildContext context) => L10().barcodeScanItem; String getOverlayText(BuildContext context) => L10().barcodeScanItem;
@ -406,11 +317,11 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async { Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
// Returned barcode must match a stock item // 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) { 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 { 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 @override
State<StatefulWidget> createState() => _QRViewState(_handler); State<StatefulWidget> createState() => _QRViewState(_handler);
@ -475,7 +453,9 @@ class InvenTreeQRView extends StatefulWidget {
class _QRViewState extends State<InvenTreeQRView> { class _QRViewState extends State<InvenTreeQRView> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); _QRViewState(this._handler) : super();
final GlobalKey qrKey = GlobalKey(debugLabel: "QR");
QRViewController? _controller; QRViewController? _controller;
@ -494,8 +474,6 @@ class _QRViewState extends State<InvenTreeQRView> {
_controller!.resumeCamera(); _controller!.resumeCamera();
} }
_QRViewState(this._handler) : super();
void _onViewCreated(BuildContext context, QRViewController controller) { void _onViewCreated(BuildContext context, QRViewController controller) {
_controller = controller; _controller = controller;
controller.scannedDataStream.listen((barcode) { 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 { class S implements WidgetsLocalizations {
const S(); const S();
static const GeneratedLocalizationsDelegate delegate = static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate();
const GeneratedLocalizationsDelegate();
static S of(BuildContext context) => static S of(BuildContext context) => Localizations.of<S>(context, WidgetsLocalizations);
Localizations.of<S>(context, WidgetsLocalizations);
@override @override
TextDirection get textDirection => TextDirection.ltr; 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 { class InvenTreeCompany extends InvenTreeModel {
InvenTreeCompany() : super();
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String get URL => "company/"; 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 @override
InvenTreeModel createFromJson(Map<String, dynamic> json) { 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 * The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
*/ */
class InvenTreeSupplierPart extends InvenTreeModel { class InvenTreeSupplierPart extends InvenTreeModel {
InvenTreeSupplierPart() : super();
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String get URL => "company/part/"; String get URL => "company/part/";
@ -79,27 +116,29 @@ class InvenTreeSupplierPart extends InvenTreeModel {
return _filters(); 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 @override
InvenTreeModel createFromJson(Map<String, dynamic> json) { InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -112,6 +151,10 @@ class InvenTreeSupplierPart extends InvenTreeModel {
class InvenTreeManufacturerPart extends InvenTreeModel { class InvenTreeManufacturerPart extends InvenTreeModel {
InvenTreeManufacturerPart() : super();
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String url = "company/part/manufacturer/"; 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; String get MPN => (jsondata["MPN"] ?? "") as String;
int get manufacturerId => (jsondata['manufacturer'] ?? -1) as int;
String get MPN => (jsondata['MPN'] ?? '') as String;
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) { InvenTreeModel createFromJson(Map<String, dynamic> json) {

View File

@ -1,18 +1,17 @@
import 'dart:async'; import "dart:async";
import 'dart:io'; import "dart:io";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/api.dart'; import "package:inventree/api.dart";
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:inventree/inventree/sentry.dart'; import "package:inventree/inventree/sentry.dart";
import 'package:inventree/widget/dialogs.dart'; import "package:inventree/widget/dialogs.dart";
import 'package:url_launcher/url_launcher.dart'; import "package:url_launcher/url_launcher.dart";
import 'package:path/path.dart' as path; import "package:path/path.dart" as path;
import 'package:http/http.dart' as http;
import '../l10.dart'; import "package:inventree/l10.dart";
import '../api_form.dart'; import "package:inventree/api_form.dart";
// Paginated response object // Paginated response object
@ -40,12 +39,17 @@ class InvenTreePageResponse {
*/ */
class InvenTreeModel { class InvenTreeModel {
InvenTreeModel();
// Construct an InvenTreeModel from a JSON data object
InvenTreeModel.fromJson(this.jsondata);
// Override the endpoint URL for each subclass // Override the endpoint URL for each subclass
String get URL => ""; String get URL => "";
// Override the web URL for each subclass // Override the web URL for each subclass
// Note: If the WEB_URL is the same (except for /api/) as URL then just leave blank // 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 { String get webUrl {
@ -114,36 +118,23 @@ class InvenTreeModel {
Map<String, dynamic> jsondata = {}; Map<String, dynamic> jsondata = {};
// Accessor for the API // Accessor for the API
var api = InvenTreeAPI(); InvenTreeAPI get api => InvenTreeAPI();
// Default empty object constructor int get pk => (jsondata["pk"] ?? -1) as int;
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;
// Some common accessors // 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" // 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)) { if (await canLaunch(webUrl)) {
await launch(webUrl); await launch(webUrl);
@ -152,7 +143,7 @@ class InvenTreeModel {
} }
} }
void openLink() async { Future <void> openLink() async {
if (link.isNotEmpty) { 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!) // Create a new object from JSON data (not a constructor!)
InvenTreeModel createFromJson(Map<String, dynamic> json) { InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -176,20 +167,60 @@ class InvenTreeModel {
String get url => "${URL}/${pk}/".replaceAll("//", "/"); String get url => "${URL}/${pk}/".replaceAll("//", "/");
// Search this Model type in the database // Search this Model type in the database
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters = const {}}) async { Future<List<InvenTreeModel>> search(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; 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 // 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 * 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); 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 // Report error
if (response.statusCode > 0) { if (response.statusCode > 0) {
@ -224,7 +255,7 @@ class InvenTreeModel {
} }
jsondata = response.data; jsondata = response.asMap();
return true; return true;
} }
@ -267,12 +298,12 @@ class InvenTreeModel {
// Override any default values // Override any default values
for (String key in filters.keys) { for (String key in filters.keys) {
params[key] = filters[key] ?? ''; params[key] = filters[key] ?? "";
} }
var response = await api.get(url, params: params); 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) { if (response.statusCode > 0) {
await sentryReportMessage( await sentryReportMessage(
@ -297,25 +328,23 @@ class InvenTreeModel {
} }
return createFromJson(response.data); return createFromJson(response.asMap());
} }
Future<InvenTreeModel?> create(Map<String, dynamic> data) async { 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')) { if (data.containsKey("id")) {
data.remove('id'); data.remove("id");
} }
var response = await api.post(URL, body: data); var response = await api.post(URL, body: data);
// Invalid response returned from server // 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) { if (response.statusCode > 0) {
await sentryReportMessage( await sentryReportMessage(
@ -340,19 +369,34 @@ class InvenTreeModel {
return null; return null;
} }
return createFromJson(response.data); return createFromJson(response.asMap());
} }
Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async { Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async {
var params = defaultListFilters(); var params = defaultListFilters();
for (String key in filters.keys) { for (String key in filters.keys) {
params[key] = filters[key] ?? ''; params[key] = filters[key] ?? "";
} }
params["limit"] = "${limit}"; params["limit"] = "${limit}";
params["offset"] = "${offset}"; 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); var response = await api.get(URL, params: params);
if (!response.isValid()) { if (!response.isValid()) {
@ -360,15 +404,17 @@ class InvenTreeModel {
} }
// Construct the response // Construct the response
InvenTreePageResponse page = new InvenTreePageResponse(); InvenTreePageResponse page = InvenTreePageResponse();
if (response.data.containsKey("count") && response.data.containsKey("results")) { var data = response.asMap();
page.count = response.data["count"] as int;
if (data.containsKey("count") && data.containsKey("results")) {
page.count = (data["count"] ?? 0) as int;
page.results = []; page.results = [];
for (var result in response.data["results"]) { for (var result in response.data["results"]) {
page.addResult(createFromJson(result)); page.addResult(createFromJson(result as Map<String, dynamic>));
} }
return page; return page;
@ -384,7 +430,7 @@ class InvenTreeModel {
var params = defaultListFilters(); var params = defaultListFilters();
for (String key in filters.keys) { for (String key in filters.keys) {
params[key] = filters[key] ?? ''; params[key] = filters[key] ?? "";
} }
var response = await api.get(URL, params: params); var response = await api.get(URL, params: params);
@ -396,20 +442,22 @@ class InvenTreeModel {
return results; return results;
} }
dynamic data; List<dynamic> data = [];
if (response.data is List) { if (response.isList()) {
data = response.data; data = response.asList();
} else if (response.data.containsKey('results')) { } else if (response.isMap()) {
data = response.data['results']; var mData = response.asMap();
} else {
data = []; if (mData.containsKey("results")) {
data = (response.data["results"] ?? []) as List<dynamic>;
}
} }
for (var d in data) { for (var d in data) {
// Create a new object (of the current class type // Create a new object (of the current class type
InvenTreeModel obj = createFromJson(d); InvenTreeModel obj = createFromJson(d as Map<String, dynamic>);
results.add(obj); results.add(obj);
} }
@ -421,9 +469,9 @@ class InvenTreeModel {
// Provide a listing of objects at the endpoint // Provide a listing of objects at the endpoint
// TODO - Static function which returns a list of objects (of this class) // 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 // Override this function for each sub-class
bool matchAgainstString(String filter) { bool matchAgainstString(String filter) {
@ -457,10 +505,11 @@ class InvenTreeModel {
class InvenTreeAttachment extends InvenTreeModel { class InvenTreeAttachment extends InvenTreeModel {
// Class representing an "attachment" file // Class representing an "attachment" file
InvenTreeAttachment() : super(); 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 // Return the filename of the attachment
String get filename { String get filename {
@ -498,25 +547,23 @@ class InvenTreeAttachment extends InvenTreeModel {
return FontAwesomeIcons.fileAlt; return FontAwesomeIcons.fileAlt;
} }
String get comment => jsondata["comment"] ?? ''; String get comment => (jsondata["comment"] ?? "") as String;
DateTime? get uploadDate { DateTime? get uploadDate {
if (jsondata.containsKey("upload_date")) { if (jsondata.containsKey("upload_date")) {
return DateTime.tryParse(jsondata["upload_date"] ?? ''); return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
} else { } else {
return null; return null;
} }
} }
InvenTreeAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async { Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async {
final APIResponse response = await InvenTreeAPI().uploadFile( final APIResponse response = await InvenTreeAPI().uploadFile(
URL, URL,
attachment, attachment,
method: 'POST', method: "POST",
name: 'attachment', name: "attachment",
fields: fields fields: fields
); );

View File

@ -1,15 +1,19 @@
import 'package:inventree/api.dart'; import "dart:io";
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'; import "package:inventree/api.dart";
import 'dart:io'; import "package:inventree/inventree/stock.dart";
import 'package:http/http.dart' as http; import "package:inventree/inventree/company.dart";
import "package:flutter/cupertino.dart";
import "package:inventree/l10.dart";
import "model.dart";
class InvenTreePartCategory extends InvenTreeModel { class InvenTreePartCategory extends InvenTreeModel {
InvenTreePartCategory() : super();
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String get URL => "part/category/"; String get URL => "part/category/";
@ -25,21 +29,20 @@ class InvenTreePartCategory extends InvenTreeModel {
@override @override
Map<String, String> defaultListFilters() { Map<String, String> defaultListFilters() {
var filters = new Map<String, String>();
filters["active"] = "true"; return {
filters["cascade"] = "false"; "active": "true",
"cascade": "false"
return filters; };
} }
String get pathstring => jsondata['pathstring'] ?? ''; String get pathstring => (jsondata["pathstring"] ?? "") as String;
String get parentpathstring { String get parentpathstring {
// TODO - Drive the refactor tractor through this // 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(); psplit.removeLast();
} }
@ -52,11 +55,7 @@ class InvenTreePartCategory extends InvenTreeModel {
return p; return p;
} }
int get partcount => jsondata['parts'] ?? 0; int get partcount => (jsondata["parts"] ?? 0) as int;
InvenTreePartCategory() : super();
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) { InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -71,25 +70,23 @@ class InvenTreePartCategory extends InvenTreeModel {
class InvenTreePartTestTemplate 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() : super();
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) : super.fromJson(json); 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 @override
InvenTreeModel createFromJson(Map<String, dynamic> json) { InvenTreeModel createFromJson(Map<String, dynamic> json) {
var template = InvenTreePartTestTemplate.fromJson(json); var template = InvenTreePartTestTemplate.fromJson(json);
@ -125,6 +122,10 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
class InvenTreePart extends InvenTreeModel { class InvenTreePart extends InvenTreeModel {
InvenTreePart() : super();
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String get URL => "part/"; String get URL => "part/";
@ -138,9 +139,9 @@ class InvenTreePart extends InvenTreeModel {
"keywords": {}, "keywords": {},
"link": {}, "link": {},
// Parent category "category": {},
"category": {
}, "default_location": {},
"units": {}, "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 // Request supplier parts for this part
Future<List<InvenTreeSupplierPart>> getSupplierParts() async { 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 // 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 { String get onOrderString {
@ -254,7 +257,7 @@ class InvenTreePart extends InvenTreeModel {
} }
// Get the stock count for this Part // 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 { String get inStockString {
@ -271,69 +274,69 @@ class InvenTreePart extends InvenTreeModel {
return q; return q;
} }
String get units => jsondata["units"] ?? ""; String get units => (jsondata["units"] ?? "") as String;
// Get the number of units being build for this Part // 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) // 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) // 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 // 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 // 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) // Get the category ID for the Part instance (or "null" if does not exist)
int get categoryId => (jsondata['category'] ?? -1) as int; int get categoryId => (jsondata["category"] ?? -1) as int;
// Get the category name for the Part instance // Get the category name for the Part instance
String get categoryName { String get categoryName {
// Inavlid category ID // 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 // Get the category description for the Part instance
String get categoryDescription { String get categoryDescription {
// Invalid category ID // 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 // 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 // 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 // Return the fully-qualified name for the Part instance
String get fullname { String get fullname {
String fn = jsondata['full_name'] ?? ''; String fn = (jsondata["full_name"] ?? "") as String;
if (fn.isNotEmpty) return fn; if (fn.isNotEmpty) return fn;
@ -369,21 +372,15 @@ class InvenTreePart extends InvenTreeModel {
final APIResponse response = await InvenTreeAPI().uploadFile( final APIResponse response = await InvenTreeAPI().uploadFile(
url, url,
image, image,
method: 'PATCH', method: "PATCH",
name: 'image', name: "image",
); );
return response.successful(); return response.successful();
} }
// Return the "starred" status of this part // Return the "starred" status of this part
bool get starred => (jsondata['starred'] ?? false) as bool; bool get starred => (jsondata["starred"] ?? false) as bool;
InvenTreePart() : super();
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
// TODO
}
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) { InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -399,11 +396,11 @@ class InvenTreePartAttachment extends InvenTreeAttachment {
InvenTreePartAttachment() : super(); InvenTreePartAttachment() : super();
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String get URL => "part/attachment/"; String get URL => "part/attachment/";
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) { InvenTreeModel createFromJson(Map<String, dynamic> json) {
return InvenTreePartAttachment.fromJson(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:device_info_plus/device_info_plus.dart";
import 'package:package_info_plus/package_info_plus.dart'; import "package:package_info_plus/package_info_plus.dart";
import 'package:sentry_flutter/sentry_flutter.dart'; import "package:sentry_flutter/sentry_flutter.dart";
import 'package:inventree/api.dart'; import "package:inventree/api.dart";
Future<Map<String, dynamic>> getDeviceInfo() async { Future<Map<String, dynamic>> getDeviceInfo() async {
@ -18,35 +18,35 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
final iosDeviceInfo = await deviceInfo.iosInfo; final iosDeviceInfo = await deviceInfo.iosInfo;
device_info = { device_info = {
'name': iosDeviceInfo.name, "name": iosDeviceInfo.name,
'model': iosDeviceInfo.model, "model": iosDeviceInfo.model,
'systemName': iosDeviceInfo.systemName, "systemName": iosDeviceInfo.systemName,
'systemVersion': iosDeviceInfo.systemVersion, "systemVersion": iosDeviceInfo.systemVersion,
'localizedModel': iosDeviceInfo.localizedModel, "localizedModel": iosDeviceInfo.localizedModel,
'utsname': iosDeviceInfo.utsname.sysname, "utsname": iosDeviceInfo.utsname.sysname,
'identifierForVendor': iosDeviceInfo.identifierForVendor, "identifierForVendor": iosDeviceInfo.identifierForVendor,
'isPhysicalDevice': iosDeviceInfo.isPhysicalDevice, "isPhysicalDevice": iosDeviceInfo.isPhysicalDevice,
}; };
} else if (Platform.isAndroid) { } else if (Platform.isAndroid) {
final androidDeviceInfo = await deviceInfo.androidInfo; final androidDeviceInfo = await deviceInfo.androidInfo;
device_info = { device_info = {
'type': androidDeviceInfo.type, "type": androidDeviceInfo.type,
'model': androidDeviceInfo.model, "model": androidDeviceInfo.model,
'device': androidDeviceInfo.device, "device": androidDeviceInfo.device,
'id': androidDeviceInfo.id, "id": androidDeviceInfo.id,
'androidId': androidDeviceInfo.androidId, "androidId": androidDeviceInfo.androidId,
'brand': androidDeviceInfo.brand, "brand": androidDeviceInfo.brand,
'display': androidDeviceInfo.display, "display": androidDeviceInfo.display,
'hardware': androidDeviceInfo.hardware, "hardware": androidDeviceInfo.hardware,
'manufacturer': androidDeviceInfo.manufacturer, "manufacturer": androidDeviceInfo.manufacturer,
'product': androidDeviceInfo.product, "product": androidDeviceInfo.product,
'version': androidDeviceInfo.version.release, "version": androidDeviceInfo.version.release,
'supported32BitAbis': androidDeviceInfo.supported32BitAbis, "supported32BitAbis": androidDeviceInfo.supported32BitAbis,
'supported64BitAbis': androidDeviceInfo.supported64BitAbis, "supported64BitAbis": androidDeviceInfo.supported64BitAbis,
'supportedAbis': androidDeviceInfo.supportedAbis, "supportedAbis": androidDeviceInfo.supportedAbis,
'isPhysicalDevice': androidDeviceInfo.isPhysicalDevice, "isPhysicalDevice": androidDeviceInfo.isPhysicalDevice,
}; };
} }
@ -90,7 +90,7 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
if (isInDebugMode()) { if (isInDebugMode()) {
print('----- In dev mode. Not sending message to Sentry.io -----'); print("----- In dev mode. Not sending message to Sentry.io -----");
return true; return true;
} }
@ -117,7 +117,7 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
Future<void> sentryReportError(dynamic error, dynamic stackTrace) async { Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
print('----- Sentry Intercepted error: $error -----'); print("----- Sentry Intercepted error: $error -----");
print(stackTrace); print(stackTrace);
// Errors thrown in development mode are unlikely to be interesting. You can // 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. // the report.
if (isInDebugMode()) { if (isInDebugMode()) {
print('----- In dev mode. Not sending report to Sentry.io -----'); print("----- In dev mode. Not sending report to Sentry.io -----");
return; return;
} }

View File

@ -1,19 +1,23 @@
import 'package:intl/intl.dart'; import "dart:async";
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 "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 "package:inventree/inventree/model.dart";
import 'dart:io'; import "package:inventree/l10.dart";
import 'package:inventree/api.dart'; import "package:inventree/api.dart";
class InvenTreeStockItemTestResult extends InvenTreeModel { class InvenTreeStockItemTestResult extends InvenTreeModel {
InvenTreeStockItemTestResult() : super();
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String get URL => "stock/test/"; 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"] ?? "") as String;
String get date => jsondata['date'] ?? '';
InvenTreeStockItemTestResult() : super();
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) { InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
@ -60,6 +58,10 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
class InvenTreeStockItem extends InvenTreeModel { class InvenTreeStockItem extends InvenTreeModel {
InvenTreeStockItem() : super();
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
// Stock status codes // Stock status codes
static const int OK = 10; static const int OK = 10;
static const int ATTENTION = 50; static const int ATTENTION = 50;
@ -97,7 +99,7 @@ class InvenTreeStockItem extends InvenTreeModel {
Color get statusColor { Color get statusColor {
switch (status) { switch (status) {
case OK: case OK:
return Color(0xFF50aa51); return Colors.black;
case ATTENTION: case ATTENTION:
return Color(0xFFfdc82a); return Color(0xFFfdc82a);
case DAMAGED: case DAMAGED:
@ -114,7 +116,7 @@ class InvenTreeStockItem extends InvenTreeModel {
String get URL => "stock/"; String get URL => "stock/";
@override @override
String WEB_URL = "stock/item/"; String get WEB_URL => "stock/item/";
@override @override
Map<String, dynamic> formFields() { Map<String, dynamic> formFields() {
@ -132,33 +134,24 @@ class InvenTreeStockItem extends InvenTreeModel {
@override @override
Map<String, String> defaultGetFilters() { Map<String, String> defaultGetFilters() {
var headers = new Map<String, String>(); return {
"part_detail": "true",
headers["part_detail"] = "true"; "location_detail": "true",
headers["location_detail"] = "true"; "supplier_detail": "true",
headers["supplier_detail"] = "true"; "cascade": "false"
headers["cascade"] = "false"; };
return headers;
} }
@override @override
Map<String, String> defaultListFilters() { Map<String, String> defaultListFilters() {
var headers = new Map<String, String>(); return {
"part_detail": "true",
headers["part_detail"] = "true"; "location_detail": "true",
headers["location_detail"] = "true"; "supplier_detail": "true",
headers["supplier_detail"] = "true"; "cascade": "false",
headers["cascade"] = "false"; "in_stock": "true",
};
return headers;
}
InvenTreeStockItem() : super();
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
// TODO
} }
List<InvenTreePartTestTemplate> testTemplates = []; 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 { bool get hasPurchasePrice {
@ -223,12 +216,14 @@ class InvenTreeStockItem extends InvenTreeModel {
return pp.isNotEmpty && pp.trim() != "-"; 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 // Date of last update
DateTime? get updatedDate { DateTime? get updatedDate {
if (jsondata.containsKey("updated")) { if (jsondata.containsKey("updated")) {
return DateTime.tryParse(jsondata["updated"] ?? ''); return DateTime.tryParse((jsondata["updated"] ?? "") as String);
} else { } else {
return null; return null;
} }
@ -248,7 +243,7 @@ class InvenTreeStockItem extends InvenTreeModel {
DateTime? get stocktakeDate { DateTime? get stocktakeDate {
if (jsondata.containsKey("stocktake_date")) { if (jsondata.containsKey("stocktake_date")) {
return DateTime.tryParse(jsondata["stocktake_date"] ?? ''); return DateTime.tryParse((jsondata["stocktake_date"] ?? "") as String);
} else { } else {
return null; return null;
} }
@ -268,45 +263,45 @@ class InvenTreeStockItem extends InvenTreeModel {
String get partName { String get partName {
String nm = ''; String nm = "";
// Use the detailed part information as priority // Use the detailed part information as priority
if (jsondata.containsKey('part_detail')) { if (jsondata.containsKey("part_detail")) {
nm = jsondata['part_detail']['full_name'] ?? ''; nm = (jsondata["part_detail"]["full_name"] ?? "") as String;
} }
// Backup if first value fails // Backup if first value fails
if (nm.isEmpty) { if (nm.isEmpty) {
nm = jsondata['part__name'] ?? ''; nm = (jsondata["part__name"] ?? "") as String;
} }
return nm; return nm;
} }
String get partDescription { String get partDescription {
String desc = ''; String desc = "";
// Use the detailed part description as priority // Use the detailed part description as priority
if (jsondata.containsKey('part_detail')) { if (jsondata.containsKey("part_detail")) {
desc = jsondata['part_detail']['description'] ?? ''; desc = (jsondata["part_detail"]["description"] ?? "") as String;
} }
if (desc.isEmpty) { if (desc.isEmpty) {
desc = jsondata['part__description'] ?? ''; desc = (jsondata["part__description"] ?? "") as String;
} }
return desc; return desc;
} }
String get partImage { String get partImage {
String img = ''; String img = "";
if (jsondata.containsKey('part_detail')) { if (jsondata.containsKey("part_detail")) {
img = jsondata['part_detail']['thumbnail'] ?? ''; img = (jsondata["part_detail"]["thumbnail"] ?? "") as String;
} }
if (img.isEmpty) { if (img.isEmpty) {
img = jsondata['part__thumbnail'] ?? ''; img = (jsondata["part__thumbnail"] ?? "") as String;
} }
return img; return img;
@ -319,107 +314,97 @@ class InvenTreeStockItem extends InvenTreeModel {
String thumb = ""; 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) { if (thumb.isEmpty) {
thumb = jsondata['part_detail']?['image'] ?? ''; thumb = (jsondata["part_detail"]?["image"] ?? "") as String;
} }
// Try a different approach // Try a different approach
if (thumb.isEmpty) { 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; if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb;
return thumb; return thumb;
} }
int get supplierPartId => (jsondata['supplier_part'] ?? -1) as int; int get supplierPartId => (jsondata["supplier_part"] ?? -1) as int;
String get supplierImage { String get supplierImage {
String thumb = ''; String thumb = "";
if (jsondata.containsKey("supplier_detail")) { if (jsondata.containsKey("supplier_detail")) {
thumb = jsondata['supplier_detail']['supplier_logo'] ?? ''; thumb = (jsondata["supplier_detail"]["supplier_logo"] ?? "") as String;
} }
return thumb; return thumb;
} }
String get supplierName { String get supplierName {
String sname = ''; String sname = "";
if (jsondata.containsKey("supplier_detail")) { if (jsondata.containsKey("supplier_detail")) {
sname = jsondata["supplier_detail"]["supplier_name"] ?? ''; sname = (jsondata["supplier_detail"]["supplier_name"] ?? "") as String;
} }
return sname; return sname;
} }
String get units { String get units {
return jsondata['part_detail']?['units'] ?? ''; return (jsondata["part_detail"]?["units"] ?? "") as String;
} }
String get supplierSKU { String get supplierSKU {
String sku = ''; String sku = "";
if (jsondata.containsKey("supplier_detail")) { if (jsondata.containsKey("supplier_detail")) {
sku = jsondata["supplier_detail"]["SKU"] ?? ''; sku = (jsondata["supplier_detail"]["SKU"] ?? "") as String;
} }
return sku; 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 (includeUnits && units.isNotEmpty) {
if (quantity.toInt() == quantity) {
q = quantity.toInt().toString();
}
if (units.isNotEmpty) {
q += " ${units}"; q += " ${units}";
} }
return q; 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; bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
String serialOrQuantityDisplay() { String serialOrQuantityDisplay() {
if (isSerialized()) { if (isSerialized()) {
return 'SN ${serialNumber}'; return "SN ${serialNumber}";
} }
// Is an integer? return simpleNumberString(quantity);
if (quantity.toInt() == quantity) {
return '${quantity.toInt()}';
}
return '${quantity}';
} }
String get locationName { 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 // Old-style name
if (loc.isEmpty) { if (loc.isEmpty) {
loc = jsondata['location__name'] ?? ''; loc = (jsondata["location__name"] ?? "") as String;
} }
return loc; return loc;
@ -427,9 +412,9 @@ class InvenTreeStockItem extends InvenTreeModel {
String get locationPathString { 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) { if (_loc.isNotEmpty) {
return _loc; return _loc;
@ -444,7 +429,7 @@ class InvenTreeStockItem extends InvenTreeModel {
if (serialNumber.isNotEmpty) { if (serialNumber.isNotEmpty) {
return "SN: $serialNumber"; return "SN: $serialNumber";
} else { } else {
return quantityString; return simpleNumberString(quantity);
} }
} }
@ -481,7 +466,7 @@ class InvenTreeStockItem extends InvenTreeModel {
"pk": "${pk}", "pk": "${pk}",
"quantity": "${q}", "quantity": "${q}",
}, },
"notes": notes ?? '', "notes": notes ?? "",
}, },
expectedStatusCode: 200 expectedStatusCode: 200
); );
@ -489,6 +474,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return response.isValid(); 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 { Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/count/", q, notes: notes); final bool result = await adjustStock(context, "/stock/count/", q, notes: notes);
@ -496,6 +482,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return result; return result;
} }
// TODO: Refactor this once the server supports API metadata for this action
Future<bool> addStock(BuildContext context, double q, {String? notes}) async { Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes); final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
@ -503,6 +490,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return result; return result;
} }
// TODO: Refactor this once the server supports API metadata for this action
Future<bool> removeStock(BuildContext context, double q, {String? notes}) async { Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes); final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
@ -510,6 +498,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return result; return result;
} }
// TODO: Refactor this once the server supports API metadata for this action
Future<bool> transferStock(int location, {double? quantity, String? notes}) async { Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) { if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) {
quantity = this.quantity; quantity = this.quantity;
@ -535,10 +524,14 @@ class InvenTreeStockItem extends InvenTreeModel {
class InvenTreeStockLocation extends InvenTreeModel { class InvenTreeStockLocation extends InvenTreeModel {
InvenTreeStockLocation() : super();
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String get URL => "stock/location/"; String get URL => "stock/location/";
String get pathstring => jsondata['pathstring'] ?? ''; String get pathstring => (jsondata["pathstring"] ?? "") as String;
@override @override
Map<String, dynamic> formFields() { Map<String, dynamic> formFields() {
@ -551,13 +544,13 @@ class InvenTreeStockLocation extends InvenTreeModel {
String get parentpathstring { String get parentpathstring {
// TODO - Drive the refactor tractor through this // 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(); psplit.removeLast();
} }
String p = psplit.join('/'); String p = psplit.join("/");
if (p.isEmpty) { if (p.isEmpty) {
p = "Top level stock location"; p = "Top level stock location";
@ -566,11 +559,7 @@ class InvenTreeStockLocation extends InvenTreeModel {
return p; return p;
} }
int get itemcount => jsondata['items'] ?? 0; int get itemcount => (jsondata["items"] ?? 0) as int;
InvenTreeStockLocation() : super();
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) { 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.dart";
import 'package:flutter_gen/gen_l10n/app_localizations_en.dart'; import "package:flutter_gen/gen_l10n/app_localizations_en.dart";
import 'package:one_context/one_context.dart'; import "package:one_context/one_context.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
// Shortcut function to reduce boilerplate! // Shortcut function to reduce boilerplate!
I18N L10() 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_localizations/flutter_localizations.dart'; import "package:flutter_gen/gen_l10n/app_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/cupertino.dart'; import "package:flutter/material.dart";
import 'package:flutter/material.dart'; import "package:one_context/one_context.dart";
import 'package:one_context/one_context.dart'; import "package:package_info_plus/package_info_plus.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:inventree/inventree/sentry.dart";
import "package:inventree/dsn.dart";
import 'package:flutter/foundation.dart'; import "package:inventree/widget/home.dart";
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async { Future<void> main() async {
@ -75,24 +74,24 @@ class InvenTreeApp extends StatelessWidget {
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: [ supportedLocales: [
const Locale('de', ''), // German const Locale("de", ""), // German
const Locale('el', ''), // Greek const Locale("el", ""), // Greek
const Locale('en', ''), // English const Locale("en", ""), // English
const Locale('es', ''), // Spanish const Locale("es", ""), // Spanish
const Locale('fr', ''), // French const Locale("fr", ""), // French
const Locale('he', ''), // Hebrew const Locale("he", ""), // Hebrew
const Locale('it', ''), // Italian const Locale("it", ""), // Italian
const Locale('ja', ''), // Japanese const Locale("ja", ""), // Japanese
const Locale('ko', ''), // Korean const Locale("ko", ""), // Korean
const Locale('nl', ''), // Dutch const Locale("nl", ""), // Dutch
const Locale('no', ''), // Norwegian const Locale("no", ""), // Norwegian
const Locale('pl', ''), // Polish const Locale("pl", ""), // Polish
const Locale('ru', ''), // Russian const Locale("ru", ""), // Russian
const Locale('sv', ''), // Swedish const Locale("sv", ""), // Swedish
const Locale('th', ''), // Thai const Locale("th", ""), // Thai
const Locale('tr', ''), // Turkish const Locale("tr", ""), // Turkish
const Locale('vi', ''), // Vietnamese const Locale("vi", ""), // Vietnamese
const Locale('zh-CN', ''), // Chinese const Locale("zh-CN", ""), // Chinese
], ],
); );

View File

@ -1,20 +1,22 @@
import 'package:path_provider/path_provider.dart'; import "dart:async";
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart'; import "package:path_provider/path_provider.dart";
import 'package:path/path.dart'; import "package:sembast/sembast.dart";
import 'dart:async'; import "package:sembast/sembast_io.dart";
import "package:path/path.dart";
/* /*
* Class for storing InvenTree preferences in a NoSql DB * Class for storing InvenTree preferences in a NoSql DB
*/ */
class InvenTreePreferencesDB { class InvenTreePreferencesDB {
InvenTreePreferencesDB._();
static final InvenTreePreferencesDB _singleton = InvenTreePreferencesDB._(); static final InvenTreePreferencesDB _singleton = InvenTreePreferencesDB._();
static InvenTreePreferencesDB get instance => _singleton; static InvenTreePreferencesDB get instance => _singleton;
InvenTreePreferencesDB._();
Completer<Database> _dbOpenCompleter = Completer(); Completer<Database> _dbOpenCompleter = Completer();
bool isOpen = false; bool isOpen = false;
@ -34,7 +36,7 @@ class InvenTreePreferencesDB {
return _dbOpenCompleter.future; return _dbOpenCompleter.future;
} }
Future _openDatabase() async { Future<void> _openDatabase() async {
// Get a platform-specific directory where persistent app data can be stored // Get a platform-specific directory where persistent app data can be stored
final appDocumentDir = await getApplicationDocumentsDirectory(); final appDocumentDir = await getApplicationDocumentsDirectory();
@ -43,7 +45,7 @@ class InvenTreePreferencesDB {
print("Path: ${appDocumentDir.path}"); print("Path: ${appDocumentDir.path}");
// Path with the form: /platform-specific-directory/demo.db // 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); final database = await databaseFactoryIo.openDatabase(dbPath);
@ -54,8 +56,14 @@ class InvenTreePreferencesDB {
class InvenTreePreferences { class InvenTreePreferences {
factory InvenTreePreferences() {
return _api;
}
InvenTreePreferences._internal();
/* The following settings are not stored to persistent storage, /* 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. * They are kept here as a convenience only.
*/ */
@ -72,11 +80,6 @@ class InvenTreePreferences {
bool expandStockList = true; bool expandStockList = true;
// Ensure we only ever create a single instance of the preferences class // 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/api.dart";
import 'package:inventree/app_colors.dart'; import "package:inventree/app_colors.dart";
import 'package:inventree/settings/release.dart'; import "package:inventree/settings/release.dart";
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter/services.dart'; import "package:flutter/services.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:package_info_plus/package_info_plus.dart'; import "package:package_info_plus/package_info_plus.dart";
import 'package:inventree/l10.dart'; import "package:inventree/l10.dart";
class InvenTreeAboutWidget extends StatelessWidget { class InvenTreeAboutWidget extends StatelessWidget {
const InvenTreeAboutWidget(this.info) : super();
final PackageInfo info; final PackageInfo info;
InvenTreeAboutWidget(this.info) : super(); Future <void> _releaseNotes(BuildContext context) async {
void _releaseNotes(BuildContext context) async {
// Load release notes from external file // Load release notes from external file
String notes = await rootBundle.loadString("assets/release_notes.md"); 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"); String notes = await rootBundle.loadString("assets/credits.md");

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@
/* /*
* Class for InvenTree user / login details * Class for InvenTree user / login details
*/ */
import 'package:sembast/sembast.dart'; import "package:sembast/sembast.dart";
import 'preferences.dart'; import "preferences.dart";
class UserProfile { class UserProfile {
@ -16,6 +16,15 @@ class UserProfile {
this.selected = false, 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 // ID of the profile
int? key; int? key;
@ -36,15 +45,6 @@ class UserProfile {
// User ID (will be provided by the server on log-in) // User ID (will be provided by the server on log-in)
int user_id = -1; 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() => { Map<String, dynamic> toJson() => {
"name": name, "name": name,
"server": server, "server": server,
@ -62,7 +62,7 @@ class UserProfileDBManager {
final store = StoreRef("profiles"); 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 { Future<bool> profileNameExists(String name) async {
@ -70,10 +70,10 @@ class UserProfileDBManager {
final profiles = await store.find(await _db, finder: finder); 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 // Check if a profile already exists with the name
final bool exists = await profileNameExists(profile.name); final bool exists = await profileNameExists(profile.name);
@ -83,7 +83,7 @@ class UserProfileDBManager {
return; 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}'"); print("Added user profile <${key}> - '${profile.name}'");
@ -91,7 +91,7 @@ class UserProfileDBManager {
profile.key = key; profile.key = key;
} }
Future selectProfile(int key) async { Future<void> selectProfile(int key) async {
/* /*
* Mark the particular profile as selected * Mark the particular profile as selected
*/ */
@ -101,7 +101,7 @@ class UserProfileDBManager {
return result; return result;
} }
Future updateProfile(UserProfile profile) async { Future<void> updateProfile(UserProfile profile) async {
if (profile.key == null) { if (profile.key == null) {
await addProfile(profile); await addProfile(profile);
@ -115,7 +115,7 @@ class UserProfileDBManager {
return result; return result;
} }
Future deleteProfile(UserProfile profile) async { Future<void> deleteProfile(UserProfile profile) async {
await store.record(profile.key).delete(await _db); await store.record(profile.key).delete(await _db);
print("Deleted user profile <${profile.key}> - '${profile.name}'"); print("Deleted user profile <${profile.key}> - '${profile.name}'");
} }
@ -135,8 +135,8 @@ class UserProfileDBManager {
if (profiles[idx].key is int && profiles[idx].key == selected) { if (profiles[idx].key is int && profiles[idx].key == selected) {
return UserProfile.fromJson( return UserProfile.fromJson(
profiles[idx].key, profiles[idx].key as int,
profiles[idx].value, profiles[idx].value as Map<String, dynamic>,
profiles[idx].key == selected, profiles[idx].key == selected,
); );
} }
@ -161,8 +161,8 @@ class UserProfileDBManager {
if (profiles[idx].key is int) { if (profiles[idx].key is int) {
profileList.add( profileList.add(
UserProfile.fromJson( UserProfile.fromJson(
profiles[idx].key, profiles[idx].key as int,
profiles[idx].value, profiles[idx].value as Map<String, dynamic>,
profiles[idx].key == selected, 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:font_awesome_flutter/font_awesome_flutter.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: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 { class CategoryDisplayWidget extends StatefulWidget {
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key); const CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
final InvenTreePartCategory? category; final InvenTreePartCategory? category;
@ -32,6 +27,7 @@ class CategoryDisplayWidget extends StatefulWidget {
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
_CategoryDisplayState(this.category);
@override @override
String getAppBarTitle(BuildContext context) => L10().partCategory; String getAppBarTitle(BuildContext context) => L10().partCategory;
@ -41,7 +37,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
List<Widget> actions = []; List<Widget> actions = [];
if ((category != null) && InvenTreeAPI().checkPermission('part_category', 'change')) { if ((category != null) && InvenTreeAPI().checkPermission("part_category", "change")) {
actions.add( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.edit), icon: FaIcon(FontAwesomeIcons.edit),
@ -74,8 +70,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
); );
} }
_CategoryDisplayState(this.category);
// The local InvenTreePartCategory object // The local InvenTreePartCategory object
final InvenTreePartCategory? category; final InvenTreePartCategory? category;
@ -199,7 +193,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
if (loading) { if (loading) {
tiles.add(progressIndicator()); tiles.add(progressIndicator());
} else if (_subcategories.length == 0) { } else if (_subcategories.isEmpty) {
tiles.add(ListTile( tiles.add(ListTile(
title: Text(L10().noSubcategories), title: Text(L10().noSubcategories),
subtitle: Text( subtitle: Text(
@ -224,7 +218,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
data: { data: {
"parent": (pk > 0) ? pk : null, "parent": (pk > 0) ? pk : null,
}, },
onSuccess: (data) async { onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) { if (data.containsKey("pk")) {
var cat = InvenTreePartCategory.fromJson(data); var cat = InvenTreePartCategory.fromJson(data);
@ -252,7 +248,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
data: { data: {
"category": (pk > 0) ? pk : null "category": (pk > 0) ? pk : null
}, },
onSuccess: (data) async { onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) { if (data.containsKey("pk")) {
var part = InvenTreePart.fromJson(data); var part = InvenTreePart.fromJson(data);
@ -274,7 +272,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
getCategoryDescriptionCard(extra: false), getCategoryDescriptionCard(extra: false),
]; ];
if (InvenTreeAPI().checkPermission('part', 'add')) { if (InvenTreeAPI().checkPermission("part", "add")) {
tiles.add( tiles.add(
ListTile( ListTile(
title: Text(L10().categoryCreate), title: Text(L10().categoryCreate),
@ -298,7 +296,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
} }
} }
if (tiles.length == 0) { if (tiles.isEmpty) {
tiles.add( tiles.add(
ListTile( ListTile(
title: Text( title: Text(
@ -327,7 +325,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
); );
case 1: case 1:
return PaginatedPartList( return PaginatedPartList(
{"category": "${category?.pk ?? null}"}, {
"category": "${category?.pk ?? 'null'}"
},
); );
case 2: case 2:
return ListView( return ListView(
@ -344,9 +344,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
* Builder for displaying a list of PartCategory objects * Builder for displaying a list of PartCategory objects
*/ */
class SubcategoryList extends StatelessWidget { 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) { void _openCategory(BuildContext context, int pk) {
@ -381,170 +382,3 @@ class SubcategoryList extends StatelessWidget {
itemBuilder: _build, itemCount: _categories.length); 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.dart";
import 'package:inventree/api_form.dart'; import "package:inventree/app_colors.dart";
import 'package:inventree/app_colors.dart'; import "package:inventree/inventree/company.dart";
import 'package:inventree/inventree/company.dart'; import "package:inventree/inventree/purchase_order.dart";
import 'package:inventree/widget/refreshable_state.dart'; import "package:inventree/widget/purchase_order_list.dart";
import 'package:flutter/cupertino.dart'; import "package:inventree/widget/refreshable_state.dart";
import 'package:flutter/material.dart'; import "package:flutter/cupertino.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:flutter/material.dart";
import 'package:inventree/l10.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/l10.dart";
class CompanyDetailWidget extends StatefulWidget { 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 @override
_CompanyDetailState createState() => _CompanyDetailState(company); _CompanyDetailState createState() => _CompanyDetailState(company);
@ -27,6 +29,8 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
final InvenTreeCompany company; final InvenTreeCompany company;
List<InvenTreePurchaseOrder> outstandingOrders = [];
@override @override
String getAppBarTitle(BuildContext context) => L10().company; String getAppBarTitle(BuildContext context) => L10().company;
@ -61,9 +65,13 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
@override @override
Future<void> request() async { Future<void> request() async {
await company.reload(); 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( company.editForm(
context, context,
@ -146,6 +154,37 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
// TODO - Add list of purchase orders // TODO - Add list of purchase orders
tiles.add(Divider()); 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) { if (company.isManufacturer) {

View File

@ -1,25 +1,22 @@
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:inventree/api.dart'; import "package:inventree/api.dart";
import 'package:inventree/inventree/company.dart'; import "package:inventree/inventree/company.dart";
import 'package:inventree/inventree/sentry.dart'; import "package:inventree/inventree/model.dart";
import 'package:inventree/widget/paginator.dart'; import "package:inventree/widget/paginator.dart";
import 'package:inventree/widget/refreshable_state.dart'; import "package:inventree/widget/refreshable_state.dart";
import 'package:inventree/widget/company_detail.dart'; import "package:inventree/widget/company_detail.dart";
import '../l10.dart';
class CompanyListWidget extends StatefulWidget { 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 @override
_CompanyListWidgetState createState() => _CompanyListWidgetState(title, filters); _CompanyListWidgetState createState() => _CompanyListWidgetState(title, filters);
@ -49,103 +46,32 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
class PaginatedCompanyList extends StatefulWidget { class PaginatedCompanyList extends StatefulWidget {
PaginatedCompanyList(this.filters, {this.onTotalChanged}); const PaginatedCompanyList(this.filters, {this.onTotalChanged});
final Map<String, String> filters; final Map<String, String> filters;
Function(int)? onTotalChanged; final Function(int)? onTotalChanged;
@override @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); _CompanyListState(Map<String, String> filters) : super(filters);
static const _pageSize = 25;
String _searchTerm = "";
Function(int)? onTotalChanged;
final Map<String, String> filters;
final PagingController<int, InvenTreeCompany> _pagingController = PagingController(firstPageKey: 0);
final TextEditingController searchController = TextEditingController();
@override @override
void initState() { Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState(); final page = await InvenTreeCompany().listPaginated(limit, offset, filters: params);
return page;
} }
@override @override
void dispose() { Widget buildItem(BuildContext context, InvenTreeModel model) {
_pagingController.dispose();
super.dispose();
}
int resultCount = 0; InvenTreeCompany company = model as InvenTreeCompany;
Future<void> _fetchPage(int pageKey) async {
try {
Map<String, String> params = filters;
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) {
return ListTile( return ListTile(
title: Text(company.name), 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/app_settings.dart";
import 'package:inventree/widget/snacks.dart'; import "package:inventree/widget/snacks.dart";
import 'package:audioplayers/audioplayers.dart'; import "package:audioplayers/audioplayers.dart";
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/l10.dart'; import "package:inventree/l10.dart";
import 'package:one_context/one_context.dart'; import "package:one_context/one_context.dart";
Future<void> confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async { 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/api.dart";
import 'package:inventree/barcode.dart'; import "package:inventree/barcode.dart";
import 'package:inventree/widget/company_list.dart'; import "package:flutter/material.dart";
import 'package:inventree/widget/search.dart'; import "package:inventree/l10.dart";
import 'package:flutter/material.dart';
import 'package:inventree/l10.dart';
import 'package:inventree/widget/category_display.dart'; import "package:inventree/settings/settings.dart";
import 'package:inventree/widget/location_display.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/widget/search.dart";
import 'package:inventree/settings/settings.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class InvenTreeDrawer extends StatelessWidget { class InvenTreeDrawer extends StatelessWidget {
final BuildContext context; const InvenTreeDrawer(this.context);
InvenTreeDrawer(this.context); final BuildContext context;
void _closeDrawer() { void _closeDrawer() {
// Close the drawer // Close the drawer
@ -29,7 +25,9 @@ class InvenTreeDrawer extends StatelessWidget {
void _home() { void _home() {
_closeDrawer(); _closeDrawer();
Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false); while (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
} }
void _search() { void _search() {
@ -38,65 +36,25 @@ class InvenTreeDrawer extends StatelessWidget {
_closeDrawer(); _closeDrawer();
showSearch( Navigator.push(
context: context, context,
delegate: PartSearchDelegate(context) MaterialPageRoute(
builder: (context) => SearchWidget()
)
); );
//Navigator.push(context, MaterialPageRoute(builder: (context) => SearchWidget()));
} }
/* /*
* Launch the camera to scan a QR code. * Launch the camera to scan a QR code.
* Upon successful scan, data are passed off to be decoded. * Upon successful scan, data are passed off to be decoded.
*/ */
void _scan() async { Future <void> _scan() async {
if (!InvenTreeAPI().checkConnection(context)) return; if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer(); _closeDrawer();
scanQrCode(context); 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 * Load settings widget
*/ */
@ -107,17 +65,14 @@ class InvenTreeDrawer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Drawer( return Drawer(
child: ListView( child: ListView(
children: ListTile.divideTiles( children: ListTile.divideTiles(
context: context, context: context,
tiles: <Widget>[ tiles: <Widget>[
ListTile( ListTile(
leading: Image.asset( leading: FaIcon(FontAwesomeIcons.home),
"assets/image/icon.png",
fit: BoxFit.scaleDown,
width: 30,
),
title: Text( title: Text(
L10().appTitle, L10().appTitle,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
@ -134,35 +89,6 @@ class InvenTreeDrawer extends StatelessWidget {
leading: FaIcon(FontAwesomeIcons.search), leading: FaIcon(FontAwesomeIcons.search),
onTap: _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( ListTile(
title: Text(L10().settings), title: Text(L10().settings),
leading: Icon(Icons.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:file_picker/file_picker.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:image_picker/image_picker.dart'; import "package:image_picker/image_picker.dart";
import 'package:inventree/l10.dart'; import "package:one_context/one_context.dart";
import 'dart:async';
import 'dart:io';
import 'package:one_context/one_context.dart';
import "package:inventree/l10.dart";
class FilePickerDialog { class FilePickerDialog {
@ -167,7 +165,7 @@ class CheckBoxField extends FormField<bool> {
class StringField extends TextFormField { class StringField extends TextFormField {
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) : StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function(String?)? validator, bool allowEmpty = false, bool isEnabled = true}) :
super( super(
decoration: InputDecoration( decoration: InputDecoration(
labelText: allowEmpty ? label : label + "*", labelText: allowEmpty ? label : label + "*",
@ -182,7 +180,7 @@ class StringField extends TextFormField {
} }
if (validator != null) { if (validator != null) {
return validator(value); return validator(value) as String?;
} }
return null; return null;
@ -196,7 +194,7 @@ class StringField extends TextFormField {
*/ */
class QuantityField 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( super(
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: label,

View File

@ -1,27 +1,28 @@
import 'package:inventree/app_colors.dart'; import "package:flutter/cupertino.dart";
import 'package:inventree/user_profile.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 { class InvenTreeHomePage extends StatefulWidget {
InvenTreeHomePage({Key? key}) : super(key: key); const InvenTreeHomePage({Key? key}) : super(key: key);
@override @override
_InvenTreeHomePageState createState() => _InvenTreeHomePageState(); _InvenTreeHomePageState createState() => _InvenTreeHomePageState();
@ -29,33 +30,27 @@ class InvenTreeHomePage extends StatefulWidget {
class _InvenTreeHomePageState extends State<InvenTreeHomePage> { class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
_InvenTreeHomePageState() : super() { _InvenTreeHomePageState() : super() {
// Initially load the profile and attempt server connection // Initially load the profile and attempt server connection
_loadProfile(); _loadProfile();
} }
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
// Selected user profile // Selected user profile
UserProfile? _profile; UserProfile? _profile;
void _searchParts() { void _search(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return; if (!InvenTreeAPI().checkConnection(context)) return;
showSearch( Navigator.push(
context: context, context,
delegate: PartSearchDelegate(context) MaterialPageRoute(
builder: (context) => SearchWidget()
)
); );
}
void _searchStock() {
if (!InvenTreeAPI().checkConnection(context)) return;
showSearch(
context: context,
delegate: StockSearchDelegate(context)
);
} }
void _scan(BuildContext context) { void _scan(BuildContext context) {
@ -64,31 +59,60 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
scanQrCode(context); scanQrCode(context);
} }
void _parts(BuildContext context) { void _showParts(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return; if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); 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; if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); 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; if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"}))); Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
} }
void _manufacturers() { void _showManufacturers(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return; if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"}))); Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
} }
void _customers() { void _showCustomers(BuildContext context) {
if (!InvenTreeAPI().checkConnection(context)) return; if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"}))); 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(); _profile = await UserProfileDBManager().getSelectedProfile();
@ -121,270 +145,181 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
setState(() {}); setState(() {});
} }
ListTile _serverTile() {
// No profile selected Widget _iconButton(BuildContext context, String label, IconData icon, {Function()? callback, String role = "", String permission = ""}) {
// Tap to select / create a profile
if (_profile == null) { bool connected = InvenTreeAPI().isConnected();
return ListTile(
title: Text(L10().profileNotSelected), bool allowed = true;
subtitle: Text(L10().profileTapToCreate),
leading: FaIcon(FontAwesomeIcons.server), if (role.isNotEmpty || permission.isNotEmpty) {
trailing: FaIcon( allowed = InvenTreeAPI().checkPermission(role, permission);
FontAwesomeIcons.user, }
color: COLOR_DANGER,
return GestureDetector(
child: Card(
margin: EdgeInsets.symmetric(
vertical: 10,
horizontal: 10
),
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: () { onTap: () {
_selectProfile();
if (!allowed) {
showSnackIcon(
L10().permissionRequired,
icon: FontAwesomeIcons.exclamationCircle,
success: false,
);
return;
}
if (callback != null) {
callback();
}
}, },
); );
} }
// Profile is selected ... List<Widget> getGridTiles(BuildContext context) {
if (InvenTreeAPI().isConnecting()) { return [
return ListTile( _iconButton(
title: Text(L10().serverConnecting), context,
subtitle: Text("${InvenTreeAPI().baseUrl}"), L10().scanBarcode,
leading: FaIcon(FontAwesomeIcons.server), FontAwesomeIcons.barcode,
trailing: Spinner( callback: () {
icon: FontAwesomeIcons.spinner, _scan(context);
color: COLOR_PROGRESS,
),
onTap: () {
_selectProfile();
} }
);
} 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: () { _iconButton(
_selectProfile(); context,
}, L10().search,
); FontAwesomeIcons.search,
} else { callback: () {
return ListTile( _search(context);
title: Text(L10().serverCouldNotConnect),
subtitle: Text("${_profile!.server}"),
leading: FaIcon(FontAwesomeIcons.server),
trailing: FaIcon(
FontAwesomeIcons.timesCircle,
color: COLOR_DANGER,
),
onTap: () {
_selectProfile();
},
);
} }
),
_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 @override
Widget build(BuildContext context) { 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( return Scaffold(
key: _homeKey, key: _homeKey,
appBar: AppBar( appBar: AppBar(
title: Text(L10().appTitle), title: Text(L10().appTitle),
actions: <Widget>[ actions: [
/*
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.search), icon: FaIcon(
tooltip: L10().search, FontAwesomeIcons.server,
onPressed: _searchParts, 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(), drawer: InvenTreeDrawer(context),
Row( body: ListView(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
children: <Widget>[ GridView.extent(
Column( maxCrossAxisExtent: 140,
children: <Widget>[ shrinkWrap: true,
IconButton( physics: ClampingScrollPhysics(),
icon: new FaIcon(FontAwesomeIcons.tools), children: getGridTiles(context),
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(),
), ),
], ],
), ),
]),
),
),
); );
} }
} }

View File

@ -1,21 +1,19 @@
import 'package:inventree/api.dart'; import "package:flutter/cupertino.dart";
import 'package:inventree/app_colors.dart'; import "package:flutter/material.dart";
import 'package:inventree/app_settings.dart'; import "package:flutter/foundation.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:inventree/widget/refreshable_state.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/widget/stock_detail.dart';
import 'package:inventree/widget/paginator.dart'; import "package:inventree/api.dart";
import 'package:inventree/l10.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 { class LocationDisplayWidget extends StatefulWidget {
@ -31,6 +29,8 @@ class LocationDisplayWidget extends StatefulWidget {
class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
_LocationDisplayState(this.location);
final InvenTreeStockLocation? location; final InvenTreeStockLocation? location;
@override @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( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.edit), icon: FaIcon(FontAwesomeIcons.edit),
@ -92,11 +92,9 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
); );
} }
_LocationDisplayState(this.location);
List<InvenTreeStockLocation> _sublocations = []; List<InvenTreeStockLocation> _sublocations = [];
String _locationFilter = ''; String _locationFilter = "";
List<InvenTreeStockLocation> get sublocations { List<InvenTreeStockLocation> get sublocations {
@ -146,7 +144,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
data: { data: {
"parent": (pk > 0) ? pk : null, "parent": (pk > 0) ? pk : null,
}, },
onSuccess: (data) async { onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) { if (data.containsKey("pk")) {
var loc = InvenTreeStockLocation.fromJson(data); var loc = InvenTreeStockLocation.fromJson(data);
@ -175,7 +176,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
data: { data: {
"location": pk, "location": pk,
}, },
onSuccess: (data) async { onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) { if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data); var item = InvenTreeStockItem.fromJson(data);
@ -280,7 +284,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
children: detailTiles(), children: detailTiles(),
); );
case 1: case 1:
return PaginatedStockList(filters); return PaginatedStockItemList(filters);
case 2: case 2:
return ListView( return ListView(
children: ListTile.divideTiles( children: ListTile.divideTiles(
@ -307,13 +311,13 @@ List<Widget> detailTiles() {
L10().sublocations, L10().sublocations,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
trailing: sublocations.length > 0 ? Text("${sublocations.length}") : null, trailing: sublocations.isNotEmpty ? Text("${sublocations.length}") : null,
), ),
]; ];
if (loading) { if (loading) {
tiles.add(progressIndicator()); tiles.add(progressIndicator());
} else if (_sublocations.length > 0) { } else if (_sublocations.isNotEmpty) {
tiles.add(SublocationList(_sublocations)); tiles.add(SublocationList(_sublocations));
} else { } else {
tiles.add(ListTile( tiles.add(ListTile(
@ -334,7 +338,7 @@ List<Widget> detailTiles() {
tiles.add(locationDescriptionCard(includeActions: false)); tiles.add(locationDescriptionCard(includeActions: false));
if (InvenTreeAPI().checkPermission('stock', 'add')) { if (InvenTreeAPI().checkPermission("stock", "add")) {
tiles.add( tiles.add(
ListTile( ListTile(
@ -362,7 +366,7 @@ List<Widget> detailTiles() {
if (location != null) { if (location != null) {
// Stock adjustment actions // Stock adjustment actions
if (InvenTreeAPI().checkPermission('stock', 'change')) { if (InvenTreeAPI().checkPermission("stock", "change")) {
// Scan items into location // Scan items into location
tiles.add( tiles.add(
ListTile( ListTile(
@ -422,9 +426,10 @@ List<Widget> detailTiles() {
class SublocationList extends StatelessWidget { 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) { void _openLocation(BuildContext context, int pk) {
@ -440,7 +445,7 @@ class SublocationList extends StatelessWidget {
InvenTreeStockLocation loc = _locations[index]; InvenTreeStockLocation loc = _locations[index];
return ListTile( return ListTile(
title: Text('${loc.name}'), title: Text("${loc.name}"),
subtitle: Text("${loc.description}"), subtitle: Text("${loc.description}"),
trailing: Text("${loc.itemcount}"), trailing: Text("${loc.itemcount}"),
onTap: () { 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:font_awesome_flutter/font_awesome_flutter.dart'; import "package:infinite_scroll_pagination/infinite_scroll_pagination.dart";
import 'package:inventree/l10.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 { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -44,9 +183,9 @@ class PaginatedSearchWidget extends StatelessWidget {
class NoResultsWidget extends StatelessWidget { class NoResultsWidget extends StatelessWidget {
final String description; const NoResultsWidget(this.description);
NoResultsWidget(this.description); final String description;
@override @override
Widget build(BuildContext context) { 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:inventree/api.dart";
import 'package:flutter/cupertino.dart'; import "package:inventree/l10.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';
class PartAttachmentsWidget extends StatefulWidget { class PartAttachmentsWidget extends StatefulWidget {
PartAttachmentsWidget(this.part, {Key? key}) : super(key: key); const PartAttachmentsWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part; final InvenTreePart part;
@ -42,7 +38,7 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
List<Widget> actions = []; List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('part', 'change')) { if (InvenTreeAPI().checkPermission("part", "change")) {
// File upload // File upload
actions.add( actions.add(
@ -127,7 +123,7 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
)); ));
} }
if (tiles.length == 0) { if (tiles.isEmpty) {
tiles.add(ListTile( tiles.add(ListTile(
title: Text(L10().attachmentNone), title: Text(L10().attachmentNone),
subtitle: Text( subtitle: Text(

View File

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

View File

@ -1,23 +1,21 @@
import 'dart:io'; import "dart:io";
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:flutter/foundation.dart'; import "package:flutter/foundation.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:image_picker/image_picker.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/api.dart";
import 'package:inventree/inventree/part.dart'; import "package:inventree/inventree/part.dart";
import 'package:inventree/widget/fields.dart'; import "package:inventree/widget/fields.dart";
import 'package:inventree/widget/refreshable_state.dart'; import "package:inventree/widget/refreshable_state.dart";
import 'package:inventree/widget/snacks.dart'; import "package:inventree/widget/snacks.dart";
import "package:inventree/l10.dart";
import '../l10.dart';
class PartImageWidget extends StatefulWidget { class PartImageWidget extends StatefulWidget {
PartImageWidget(this.part, {Key? key}) : super(key: key); const PartImageWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part; final InvenTreePart part;
@ -46,7 +44,7 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
List<Widget> actions = []; List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('part', 'change')) { if (InvenTreeAPI().checkPermission("part", "change")) {
// File upload // File upload
actions.add( 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:flutter/material.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/api.dart'; import "package:inventree/api.dart";
import 'package:inventree/inventree/part.dart'; import "package:inventree/inventree/part.dart";
import 'package:inventree/widget/refreshable_state.dart'; import "package:inventree/widget/refreshable_state.dart";
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:flutter_markdown/flutter_markdown.dart'; import "package:flutter_markdown/flutter_markdown.dart";
import 'package:inventree/l10.dart'; import "package:inventree/l10.dart";
class PartNotesWidget extends StatefulWidget { 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 @override
_PartNotesState createState() => _PartNotesState(part); _PartNotesState createState() => _PartNotesState(part);
@ -21,10 +21,10 @@ class PartNotesWidget extends StatefulWidget {
class _PartNotesState extends RefreshableState<PartNotesWidget> { class _PartNotesState extends RefreshableState<PartNotesWidget> {
final InvenTreePart part;
_PartNotesState(this.part); _PartNotesState(this.part);
final InvenTreePart part;
@override @override
Future<void> request() async { Future<void> request() async {
await part.reload(); await part.reload();
@ -38,7 +38,7 @@ class _PartNotesState extends RefreshableState<PartNotesWidget> {
List<Widget> actions = []; List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('part', 'change')) { if (InvenTreeAPI().checkPermission("part", "change")) {
actions.add( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.edit), 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/cupertino.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:inventree/inventree/part.dart'; import "package:inventree/inventree/part.dart";
import 'package:inventree/inventree/company.dart'; import "package:inventree/inventree/company.dart";
import 'package:inventree/widget/company_detail.dart'; import "package:inventree/widget/company_detail.dart";
import 'package:inventree/widget/refreshable_state.dart'; import "package:inventree/widget/refreshable_state.dart";
class PartSupplierWidget extends StatefulWidget { class PartSupplierWidget extends StatefulWidget {
PartSupplierWidget(this.part, {Key? key}) : super(key: key); const PartSupplierWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part; final InvenTreePart part;

View File

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

View File

@ -1,393 +1,347 @@
import "dart:async";
import 'package:inventree/widget/part_detail.dart'; import "package:flutter/cupertino.dart";
import 'package:inventree/widget/progress.dart'; import "package:flutter/material.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:inventree/inventree/part.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/inventree/stock.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?> { // Widget for performing database-wide search
class SearchWidget extends StatefulWidget {
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;
}
}
}
@override @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 _SearchDisplayState extends RefreshableState<SearchWidget> {
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;
}
}
}
@override @override
String get searchFieldLabel => L10().searchStock; String getAppBarTitle(BuildContext context) => L10().search;
// List of StockItem results final TextEditingController searchController = TextEditingController();
List<InvenTreeStockItem> itemResults = [];
Future<void> search(BuildContext context) async { Timer? debounceTimer;
// Search string too short!
if (query.length < 3) { int nPartResults = 0;
itemResults.clear();
showResults(context); int nCategoryResults = 0;
return;
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 (query == _cachedQuery) { if (immediate) {
return; search(text);
} } else {
debounceTimer = Timer(Duration(milliseconds: 250), () {
_cachedQuery = query; search(text);
_searching = true;
print("Searching...");
showResults(context);
// 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);
}
}
_searching = false;
showSnackIcon(
"${itemResults.length} ${L10().results}",
success: itemResults.length > 0,
icon: FontAwesomeIcons.pollH,
);
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 _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) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => StockDetailWidget(it))
);
}
}); });
} }
}
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;
});
return;
}
// Search parts
InvenTreePart().count(
searchQuery: term
).then((int n) {
setState(() {
nPartResults = n;
});
});
// Search part categories
InvenTreePartCategory().count(
searchQuery: term,
).then((int n) {
setState(() {
nCategoryResults = n;
});
});
// Search stock items
InvenTreeStockItem().count(
searchQuery: term
).then((int n) {
setState(() {
nStockResults = n;
});
});
// Search stock locations
InvenTreeStockLocation().count(
searchQuery: term
).then((int n) {
setState(() {
nLocationResults = n;
});
});
// Search suppliers
InvenTreeCompany().count(
searchQuery: term,
filters: {
"is_supplier": "true",
},
).then((int n) {
setState(() {
nSupplierResults = n;
});
});
// Search purchase orders
InvenTreePurchaseOrder().count(
searchQuery: term,
filters: {
"outstanding": "true"
}
).then((int n) {
setState(() {
nPurchaseOrderResults = n;
});
});
}
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);
},
),
)
)
); );
String query = searchController.text;
List<Widget> results = [];
// 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) => 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 @override
Widget buildResults(BuildContext context) { Widget getBody(BuildContext context) {
return Center(
search(context); child: ListView(
children: ListTile.divideTiles(
if (_searching) { context: context,
return progressIndicator(); tiles: _tiles(context),
} ).toList()
)
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> | * | Text <icon> |
*/ */
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:one_context/one_context.dart'; import "package:one_context/one_context.dart";
import 'package:inventree/l10.dart'; import "package:inventree/l10.dart";
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) { 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; Color backgroundColor = Colors.deepOrange;

View File

@ -1,13 +1,10 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/app_colors.dart'; import "package:inventree/app_colors.dart";
class Spinner extends StatefulWidget { class Spinner extends StatefulWidget {
final IconData? icon;
final Duration duration;
final Color color;
const Spinner({ const Spinner({
this.color = COLOR_GRAY_LIGHT, this.color = COLOR_GRAY_LIGHT,
@ -16,12 +13,16 @@ class Spinner extends StatefulWidget {
this.duration = const Duration(milliseconds: 1800), this.duration = const Duration(milliseconds: 1800),
}) : super(key: key); }) : super(key: key);
final IconData? icon;
final Duration duration;
final Color color;
@override @override
_SpinnerState createState() => _SpinnerState(); _SpinnerState createState() => _SpinnerState();
} }
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin { class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
AnimationController? _controller; late AnimationController? _controller;
Widget? _child; Widget? _child;
@override @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/api.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';
class StarredPartWidget extends StatefulWidget { class StarredPartWidget extends StatefulWidget {
StarredPartWidget({Key? key}) : super(key: key); const StarredPartWidget({Key? key}) : super(key: key);
@override @override
_StarredPartState createState() => _StarredPartState(); _StarredPartState createState() => _StarredPartState();
@ -73,7 +71,7 @@ class _StarredPartState extends RefreshableState<StarredPartWidget> {
return progressIndicator(); return progressIndicator();
} }
if (starredParts.length == 0) { if (starredParts.isEmpty) {
return ListView( return ListView(
children: [ children: [
ListTile( ListTile(

View File

@ -1,30 +1,30 @@
import 'package:inventree/app_colors.dart'; import "package:inventree/app_colors.dart";
import 'package:inventree/barcode.dart'; import "package:inventree/barcode.dart";
import 'package:inventree/inventree/model.dart'; import "package:inventree/inventree/model.dart";
import 'package:inventree/inventree/stock.dart'; import "package:inventree/inventree/stock.dart";
import 'package:inventree/inventree/part.dart'; import "package:inventree/inventree/part.dart";
import 'package:inventree/widget/dialogs.dart'; import "package:inventree/widget/dialogs.dart";
import 'package:inventree/widget/fields.dart'; import "package:inventree/widget/fields.dart";
import 'package:inventree/widget/location_display.dart'; import "package:inventree/widget/location_display.dart";
import 'package:inventree/widget/part_detail.dart'; import "package:inventree/widget/part_detail.dart";
import 'package:inventree/widget/progress.dart'; import "package:inventree/widget/progress.dart";
import 'package:inventree/widget/refreshable_state.dart'; import "package:inventree/widget/refreshable_state.dart";
import 'package:inventree/widget/snacks.dart'; import "package:inventree/widget/snacks.dart";
import 'package:inventree/widget/stock_item_test_results.dart'; import "package:inventree/widget/stock_item_test_results.dart";
import 'package:inventree/widget/stock_notes.dart'; import "package:inventree/widget/stock_notes.dart";
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:flutter/material.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 { class StockDetailWidget extends StatefulWidget {
StockDetailWidget(this.item, {Key? key}) : super(key: key); const StockDetailWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item; final InvenTreeStockItem item;
@ -35,6 +35,8 @@ class StockDetailWidget extends StatefulWidget {
class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
_StockItemDisplayState(this.item);
@override @override
String getAppBarTitle(BuildContext context) => L10().stockItem; String getAppBarTitle(BuildContext context) => L10().stockItem;
@ -46,14 +48,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
final _countStockKey = GlobalKey<FormState>(); final _countStockKey = GlobalKey<FormState>();
final _moveStockKey = GlobalKey<FormState>(); final _moveStockKey = GlobalKey<FormState>();
_StockItemDisplayState(this.item);
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> getAppBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('stock', 'view')) { if (InvenTreeAPI().checkPermission("stock", "view")) {
actions.add( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.globe), 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( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.edit), icon: FaIcon(FontAwesomeIcons.edit),
@ -99,13 +99,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
await item.reload(); await item.reload();
// Request part information // Request part information
part = await InvenTreePart().get(item.partId) as InvenTreePart; part = await InvenTreePart().get(item.partId) as InvenTreePart?;
// Request test results... // Request test results...
await item.getTestResults(); await item.getTestResults();
} }
void _editStockItem(BuildContext context) async { Future <void> _editStockItem(BuildContext context) async {
var fields = InvenTreeStockItem().formFields(); 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); double quantity = double.parse(_quantityController.text);
_quantityController.clear(); _quantityController.clear();
@ -138,7 +138,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
refresh(); refresh();
} }
void _addStockDialog() async { Future <void> _addStockDialog() async {
_quantityController.clear(); _quantityController.clear();
_notesController.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); double quantity = double.parse(_quantityController.text);
_quantityController.clear(); _quantityController.clear();
@ -211,7 +211,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
); );
} }
void _countStock() async { Future <void> _countStock() async {
double quantity = double.parse(_quantityController.text); double quantity = double.parse(_quantityController.text);
_quantityController.clear(); _quantityController.clear();
@ -223,9 +223,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
refresh(); refresh();
} }
void _countStockDialog() async { Future <void> _countStockDialog() async {
_quantityController.text = item.quantityString; _quantityController.text = item.quantity.toString();
_notesController.clear(); _notesController.clear();
showFormDialog(L10().countStock, 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) { if (result) {
showSnackIcon( 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; double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
String notes = _notesController.text; 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; int? location_pk;
_quantityController.text = "${item.quantityString}"; _quantityController.text = "${item.quantity}";
showFormDialog(L10().transferStock, showFormDialog(L10().transferStock,
key: _moveStockKey, key: _moveStockKey,
@ -327,13 +327,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}, },
onFind: (String filter) async { onFind: (String filter) async {
Map<String, String> _filters = { final results = await InvenTreeStockLocation().search(filter);
"search": filter,
"offset": "0",
"limit": "25"
};
final List<InvenTreeModel> results = await InvenTreeStockLocation().list(filters: _filters);
List<dynamic> items = []; List<dynamic> items = [];
@ -349,13 +343,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
hint: L10().searchLocation, hint: L10().searchLocation,
onChanged: null, onChanged: null,
itemAsString: (dynamic location) { itemAsString: (dynamic location) {
return location['pathstring']; return (location["pathstring"] ?? "") as String;
}, },
onSaved: (dynamic location) { onSaved: (dynamic location) {
if (location == null) { if (location == null) {
location_pk = null; location_pk = null;
} else { } else {
location_pk = location['pk']; location_pk = location["pk"] as int;
} }
}, },
isFilteredOnline: true, isFilteredOnline: true,
@ -420,7 +414,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
ListTile( ListTile(
title: Text(L10().quantity), title: Text(L10().quantity),
leading: FaIcon(FontAwesomeIcons.cubes), leading: FaIcon(FontAwesomeIcons.cubes),
trailing: Text("${item.quantityString}"), trailing: Text("${item.quantityString()}"),
) )
); );
} }
@ -503,7 +497,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
// Supplier part? // Supplier part?
// TODO: Display supplier part info page? // TODO: Display supplier part info page?
if (false && item.supplierPartId > 0) { /*
if (item.supplierPartId > 0) {
tiles.add( tiles.add(
ListTile( ListTile(
title: Text("${item.supplierName}"), title: Text("${item.supplierName}"),
@ -514,6 +509,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
) )
); );
} }
*/
if (item.link.isNotEmpty) { if (item.link.isNotEmpty) {
tiles.add( tiles.add(
@ -559,7 +555,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
// TODO - Is this stock item linked to a PurchaseOrder? // TODO - Is this stock item linked to a PurchaseOrder?
// TODO - Re-enable stock item history display // TODO - Re-enable stock item history display
if (false && item.trackingItemCount > 0) { /*
if (item.trackingItemCount > 0) {
tiles.add( tiles.add(
ListTile( ListTile(
title: Text(L10().history), title: Text(L10().history),
@ -574,6 +571,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
) )
); );
} }
*/
// Notes field // Notes field
tiles.add( tiles.add(
@ -600,7 +598,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
tiles.add(headerTile()); tiles.add(headerTile());
// First check that the user has the required permissions to adjust stock // First check that the user has the required permissions to adjust stock
if (!InvenTreeAPI().checkPermission('stock', 'change')) { if (!InvenTreeAPI().checkPermission("stock", "change")) {
tiles.add( tiles.add(
ListTile( ListTile(
title: Text(L10().permissionRequired), title: Text(L10().permissionRequired),
@ -624,7 +622,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
title: Text(L10().countStock), title: Text(L10().countStock),
leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK), leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK),
onTap: _countStockDialog, 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), leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
trailing: FaIcon(FontAwesomeIcons.qrcode), trailing: FaIcon(FontAwesomeIcons.qrcode),
onTap: () { onTap: () {
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( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemBarcodeAssignmentHandler(item))) MaterialPageRoute(builder: (context) => InvenTreeQRView(handler))
).then((context) { );
refresh();
});
} }
) )
); );
@ -710,12 +727,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
items: <BottomNavigationBarItem> [ items: <BottomNavigationBarItem> [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.infoCircle), icon: FaIcon(FontAwesomeIcons.infoCircle),
title: Text(L10().details), label: L10().details,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.wrench), 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/app_colors.dart'; import "package:inventree/inventree/part.dart";
import 'package:inventree/inventree/part.dart'; import "package:inventree/inventree/stock.dart";
import 'package:inventree/inventree/stock.dart'; import "package:inventree/inventree/model.dart";
import 'package:inventree/inventree/model.dart'; import "package:inventree/api.dart";
import 'package:inventree/api.dart'; import "package:inventree/widget/progress.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/l10.dart'; import "package:inventree/l10.dart";
import 'dart:io'; import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import 'package:flutter/cupertino.dart'; import "package:inventree/widget/refreshable_state.dart";
import 'package:flutter/material.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/widget/refreshable_state.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class StockItemTestResultsWidget extends StatefulWidget { class StockItemTestResultsWidget extends StatefulWidget {
StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key); const StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item; final InvenTreeStockItem item;
@ -32,7 +26,7 @@ class StockItemTestResultsWidget extends StatefulWidget {
class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestResultsWidget> { class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestResultsWidget> {
final _addResultKey = GlobalKey<FormState>(); _StockItemTestResultDisplayState(this.item);
@override @override
String getAppBarTitle(BuildContext context) => L10().testResults; String getAppBarTitle(BuildContext context) => L10().testResults;
@ -57,9 +51,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
final InvenTreeStockItem item; final InvenTreeStockItem item;
_StockItemTestResultDisplayState(this.item); Future <void> addTestResult(BuildContext context, {String name = "", bool nameIsEditable = true, bool result = false, String value = "", bool valueRequired = false, bool attachmentRequired = false}) async {
void addTestResult(BuildContext context, {String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
InvenTreeStockItemTestResult().createForm( InvenTreeStockItemTestResult().createForm(
context, context,
@ -150,7 +142,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
var results = getTestResults(); var results = getTestResults();
if (results.length == 0) { if (results.isEmpty) {
tiles.add(ListTile( tiles.add(ListTile(
title: Text(L10().testResultNone), title: Text(L10().testResultNone),
subtitle: Text(L10().testResultNoneDetail), subtitle: Text(L10().testResultNoneDetail),
@ -165,7 +157,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
String _test = ""; String _test = "";
bool _result = false; bool _result = false;
String _value = ""; String _value = "";
String _notes = "";
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE); FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
bool _valueRequired = false; bool _valueRequired = false;
@ -175,8 +166,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
_result = item.passFailStatus(); _result = item.passFailStatus();
_test = item.testName; _test = item.testName;
_required = item.required; _required = item.required;
_value = item.latestResult()?.value ?? ''; _value = item.latestResult()?.value ?? "";
_notes = item.latestResult()?.notes ?? '';
_valueRequired = item.requiresValue; _valueRequired = item.requiresValue;
_attachmentRequired = item.requiresAttachment; _attachmentRequired = item.requiresAttachment;
} else if (item is InvenTreeStockItemTestResult) { } else if (item is InvenTreeStockItemTestResult) {
@ -184,7 +174,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
_test = item.testName; _test = item.testName;
_required = false; _required = false;
_value = item.value; _value = item.value;
_notes = item.notes;
} }
if (_result == true) { 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:flutter/material.dart";
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import 'package:inventree/inventree/stock.dart'; import "package:inventree/inventree/stock.dart";
import 'package:inventree/widget/refreshable_state.dart'; import "package:inventree/widget/refreshable_state.dart";
import 'package:flutter/cupertino.dart'; import "package:flutter/cupertino.dart";
import 'package:flutter_markdown/flutter_markdown.dart'; import "package:flutter_markdown/flutter_markdown.dart";
import 'package:inventree/l10.dart'; import "package:inventree/l10.dart";
import '../api.dart'; import "package:inventree/api.dart";
class StockNotesWidget extends StatefulWidget { 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 @override
_StockNotesState createState() => _StockNotesState(item); _StockNotesState createState() => _StockNotesState(item);
@ -23,10 +23,10 @@ class StockNotesWidget extends StatefulWidget {
class _StockNotesState extends RefreshableState<StockNotesWidget> { class _StockNotesState extends RefreshableState<StockNotesWidget> {
final InvenTreeStockItem item;
_StockNotesState(this.item); _StockNotesState(this.item);
final InvenTreeStockItem item;
@override @override
String getAppBarTitle(BuildContext context) => L10().stockItemNotes; String getAppBarTitle(BuildContext context) => L10().stockItemNotes;
@ -39,7 +39,7 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> {
List<Widget> getAppBarActions(BuildContext context) { List<Widget> getAppBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
if (InvenTreeAPI().checkPermission('stock', 'change')) { if (InvenTreeAPI().checkPermission("stock", "change")) {
actions.add( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.edit), 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:inventree/l10.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';
class SubmitFeedbackWidget extends StatefulWidget { class SubmitFeedbackWidget extends StatefulWidget {
@ -18,7 +16,7 @@ class SubmitFeedbackWidget extends StatefulWidget {
class _SubmitFeedbackState extends State<SubmitFeedbackWidget> { class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
final _formkey = new GlobalKey<FormState>(); final _formkey = GlobalKey<FormState>();
String message = ""; String message = "";
@ -61,8 +59,6 @@ class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
key: _formkey, key: _formkey,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TextFormField( TextFormField(

View File

@ -49,7 +49,21 @@ packages:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted 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: camera:
dependency: "direct main" dependency: "direct main"
description: description:
@ -113,6 +127,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" 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: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -315,6 +336,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.3"
lint:
dependency: "direct dev"
description:
name: lint
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
markdown: markdown:
dependency: transitive dependency: transitive
description: description:

View File

@ -13,40 +13,43 @@ environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
dependencies: 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: flutter:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter 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 flutter_markdown: ^0.6.2 # Rendering markdown
camera: # Camera font_awesome_flutter: ^9.1.0 # FontAwesome icon set
path_provider: 2.0.2 # Local file storage http: ^0.13.0
sembast: ^3.1.0+2 # NoSQL data storage image_picker: ^0.8.3 # Select or take photos
one_context: ^1.1.0 # Dialogs without requiring context
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work! infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
audioplayers: ^0.20.1 # Play audio files intl: ^0.17.0
dropdown_search: 0.6.3 # Dropdown autocomplete form fields 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:
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: dev_dependencies:
flutter_launcher_icons:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_launcher_icons: lint: ^1.0.0
flutter_icons: flutter_icons:
android: true android: true

View File

@ -5,9 +5,9 @@
// gestures. You can also use WidgetTester to find child widgets in the widget // 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. // 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() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets("Counter increments smoke test", (WidgetTester tester) async {
}); });
} }