mirror of
https://github.com/inventree/inventree-app.git
synced 2025-05-02 15:28:53 +00:00
Merge pull request #72 from SchrodingersGat/purchase-orders
Purchase orders
This commit is contained in:
commit
91ec55967d
81
.github/workflows/test.yaml
vendored
Normal file
81
.github/workflows/test.yaml
vendored
Normal 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
2
.gitignore
vendored
@ -9,6 +9,8 @@
|
||||
.history
|
||||
.svn/
|
||||
|
||||
coverage/*
|
||||
|
||||
# Sentry API key
|
||||
lib/dsn.dart
|
||||
|
||||
|
65
analysis_options.yaml
Normal file
65
analysis_options.yaml
Normal 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
|
@ -8,7 +8,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.0'
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
@ -1,6 +1,15 @@
|
||||
## InvenTree App Release Notes
|
||||
---
|
||||
|
||||
### 0.5.0 - October 2021
|
||||
---
|
||||
|
||||
- Display Purchase Order details
|
||||
- Edit Purchase Order information
|
||||
- Display Company details (supplier / manufacturer / customer)
|
||||
- Edit Company information
|
||||
- Fixed bug relating to stock transfer for parts with specified "units"
|
||||
|
||||
### 0.4.7 - September 2021
|
||||
---
|
||||
|
||||
|
235
lib/api.dart
235
lib/api.dart
@ -1,24 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:http/http.dart" as http;
|
||||
import "package:intl/intl.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import "package:open_file/open_file.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:cached_network_image/cached_network_image.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:flutter_cache_manager/flutter_cache_manager.dart";
|
||||
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/user_profile.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:path_provider/path_provider.dart";
|
||||
|
||||
|
||||
/*
|
||||
@ -49,7 +50,32 @@ class APIResponse {
|
||||
|
||||
bool clientError() => (statusCode >= 400) && (statusCode < 500);
|
||||
|
||||
bool serverError() => (statusCode >= 500);
|
||||
bool serverError() => statusCode >= 500;
|
||||
|
||||
bool isMap() {
|
||||
return data != null && data is Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Map<String, dynamic> asMap() {
|
||||
if (isMap()) {
|
||||
return data as Map<String, dynamic>;
|
||||
} else {
|
||||
// Empty map
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
bool isList() {
|
||||
return data != null && data is List<dynamic>;
|
||||
}
|
||||
|
||||
List<dynamic> asList() {
|
||||
if (isList()) {
|
||||
return data as List<dynamic>;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -60,8 +86,6 @@ class APIResponse {
|
||||
*/
|
||||
class InvenTreeFileService extends FileService {
|
||||
|
||||
HttpClient? _client;
|
||||
|
||||
InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
|
||||
_client = client ?? HttpClient();
|
||||
|
||||
@ -73,6 +97,8 @@ class InvenTreeFileService extends FileService {
|
||||
}
|
||||
}
|
||||
|
||||
HttpClient? _client;
|
||||
|
||||
@override
|
||||
Future<FileServiceResponse> get(String url,
|
||||
{Map<String, String>? headers}) async {
|
||||
@ -107,6 +133,12 @@ class InvenTreeFileService extends FileService {
|
||||
|
||||
class InvenTreeAPI {
|
||||
|
||||
factory InvenTreeAPI() {
|
||||
return _api;
|
||||
}
|
||||
|
||||
InvenTreeAPI._internal();
|
||||
|
||||
// Minimum required API version for server
|
||||
static const _minApiVersion = 7;
|
||||
|
||||
@ -132,11 +164,12 @@ class InvenTreeAPI {
|
||||
String _makeUrl(String url) {
|
||||
|
||||
// Strip leading slash
|
||||
if (url.startsWith('/')) {
|
||||
if (url.startsWith("/")) {
|
||||
url = url.substring(1, url.length);
|
||||
}
|
||||
|
||||
url = url.replaceAll('//', '/');
|
||||
// Prevent double-slash
|
||||
url = url.replaceAll("//", "/");
|
||||
|
||||
return baseUrl + url;
|
||||
}
|
||||
@ -149,7 +182,7 @@ class InvenTreeAPI {
|
||||
if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
|
||||
return _makeUrl(endpoint);
|
||||
} else {
|
||||
return _makeUrl("/api/" + endpoint);
|
||||
return _makeUrl("/api/${endpoint}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,10 +217,10 @@ class InvenTreeAPI {
|
||||
}
|
||||
|
||||
// Server instance information
|
||||
String instance = '';
|
||||
String instance = "";
|
||||
|
||||
// Server version information
|
||||
String _version = '';
|
||||
String _version = "";
|
||||
|
||||
// API version of the connected server
|
||||
int _apiVersion = 1;
|
||||
@ -209,15 +242,14 @@ class InvenTreeAPI {
|
||||
}
|
||||
|
||||
// Ensure we only ever create a single instance of the API class
|
||||
static final InvenTreeAPI _api = new InvenTreeAPI._internal();
|
||||
static final InvenTreeAPI _api = InvenTreeAPI._internal();
|
||||
|
||||
factory InvenTreeAPI() {
|
||||
return _api;
|
||||
bool supportPoReceive() {
|
||||
|
||||
// API endpoint for receiving purchase order line items was introduced in v12
|
||||
return _apiVersion >= 12;
|
||||
}
|
||||
|
||||
InvenTreeAPI._internal();
|
||||
|
||||
|
||||
/*
|
||||
* Connect to the remote InvenTree server:
|
||||
*
|
||||
@ -239,15 +271,15 @@ class InvenTreeAPI {
|
||||
|
||||
if (address.isEmpty || username.isEmpty || password.isEmpty) {
|
||||
showSnackIcon(
|
||||
"Incomplete profile details",
|
||||
L10().incompleteDetails,
|
||||
icon: FontAwesomeIcons.exclamationCircle,
|
||||
success: false
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!address.endsWith('/')) {
|
||||
address = address + '/';
|
||||
if (!address.endsWith("/")) {
|
||||
address = address + "/";
|
||||
}
|
||||
/* TODO: Better URL validation
|
||||
* - If not a valid URL, return error
|
||||
@ -267,8 +299,10 @@ class InvenTreeAPI {
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = response.asMap();
|
||||
|
||||
// We expect certain response from the server
|
||||
if (response.data == null || !response.data.containsKey("server") || !response.data.containsKey("version") || !response.data.containsKey("instance")) {
|
||||
if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) {
|
||||
|
||||
showServerError(
|
||||
L10().missingData,
|
||||
@ -279,11 +313,11 @@ class InvenTreeAPI {
|
||||
}
|
||||
|
||||
// Record server information
|
||||
_version = response.data["version"];
|
||||
instance = response.data['instance'] ?? '';
|
||||
_version = (data["version"] ?? "") as String;
|
||||
instance = (data["instance"] ?? "") as String;
|
||||
|
||||
// Default API version is 1 if not provided
|
||||
_apiVersion = (response.data['apiVersion'] ?? 1) as int;
|
||||
_apiVersion = (data["apiVersion"] ?? 1) as int;
|
||||
|
||||
if (_apiVersion < _minApiVersion) {
|
||||
|
||||
@ -332,7 +366,9 @@ class InvenTreeAPI {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.data == null || !response.data.containsKey("token")) {
|
||||
data = response.asMap();
|
||||
|
||||
if (!data.containsKey("token")) {
|
||||
showServerError(
|
||||
L10().tokenMissing,
|
||||
L10().tokenMissingFromResponse,
|
||||
@ -342,7 +378,7 @@ class InvenTreeAPI {
|
||||
}
|
||||
|
||||
// Return the received token
|
||||
_token = response.data["token"];
|
||||
_token = (data["token"] ?? "") as String;
|
||||
print("Received token - $_token");
|
||||
|
||||
// Request user role information
|
||||
@ -358,7 +394,7 @@ class InvenTreeAPI {
|
||||
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
_token = '';
|
||||
_token = "";
|
||||
profile = null;
|
||||
}
|
||||
|
||||
@ -405,7 +441,7 @@ class InvenTreeAPI {
|
||||
|
||||
// Next we request the permissions assigned to the current user
|
||||
// Note: 2021-02-27 this "roles" feature for the API was just introduced.
|
||||
// Any 'older' version of the server allows any API method for any logged in user!
|
||||
// Any "older" version of the server allows any API method for any logged in user!
|
||||
// We will return immediately, but request the user roles in the background
|
||||
|
||||
var response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
|
||||
@ -414,9 +450,11 @@ class InvenTreeAPI {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data.containsKey('roles')) {
|
||||
var data = response.asMap();
|
||||
|
||||
if (data.containsKey("roles")) {
|
||||
// Save a local copy of the user roles
|
||||
roles = response.data['roles'];
|
||||
roles = response.data["roles"] as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -424,7 +462,7 @@ class InvenTreeAPI {
|
||||
/*
|
||||
* Check if the user has the given role.permission assigned
|
||||
*e
|
||||
* e.g. 'part', 'change'
|
||||
* e.g. "part", "change"
|
||||
*/
|
||||
|
||||
// If we do not have enough information, assume permission is allowed
|
||||
@ -437,7 +475,7 @@ class InvenTreeAPI {
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> perms = List.from(roles[role]);
|
||||
List<String> perms = List.from(roles[role] as List<dynamic>);
|
||||
return perms.contains(permission);
|
||||
} catch (error, stackTrace) {
|
||||
sentryReportError(error, stackTrace);
|
||||
@ -447,19 +485,17 @@ class InvenTreeAPI {
|
||||
|
||||
|
||||
// Perform a PATCH request
|
||||
Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int? expectedStatusCode}) async {
|
||||
var _body = Map<String, String>();
|
||||
Future<APIResponse> patch(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode}) async {
|
||||
|
||||
// Copy across provided data
|
||||
body.forEach((K, V) => _body[K] = V);
|
||||
Map<String, dynamic> _body = body;
|
||||
|
||||
HttpClientRequest? request = await apiRequest(url, "PATCH");
|
||||
|
||||
if (request == null) {
|
||||
// Return an "invalid" APIResponse
|
||||
return new APIResponse(
|
||||
return APIResponse(
|
||||
url: url,
|
||||
method: 'PATCH',
|
||||
method: "PATCH",
|
||||
error: "HttpClientRequest is null"
|
||||
);
|
||||
}
|
||||
@ -503,7 +539,7 @@ class InvenTreeAPI {
|
||||
|
||||
HttpClientRequest? _request;
|
||||
|
||||
var client = createClient(true);
|
||||
var client = createClient(allowBadCert: true);
|
||||
|
||||
// Attempt to open a connection to the server
|
||||
try {
|
||||
@ -511,8 +547,8 @@ class InvenTreeAPI {
|
||||
|
||||
// Set headers
|
||||
_request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
|
||||
_request.headers.set(HttpHeaders.acceptHeader, 'application/json');
|
||||
_request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
|
||||
_request.headers.set(HttpHeaders.acceptHeader, "application/json");
|
||||
_request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
|
||||
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
|
||||
|
||||
} on SocketException catch (error) {
|
||||
@ -550,7 +586,7 @@ class InvenTreeAPI {
|
||||
showServerError(L10().connectionRefused, error.toString());
|
||||
} on TimeoutException {
|
||||
showTimeoutError();
|
||||
} catch (error, stackTrace) {
|
||||
} catch (error) {
|
||||
print("Error downloading image:");
|
||||
print(error.toString());
|
||||
showServerError(L10().downloadError, error.toString());
|
||||
@ -561,7 +597,7 @@ class InvenTreeAPI {
|
||||
* Upload a file to the given URL
|
||||
*/
|
||||
Future<APIResponse> uploadFile(String url, File f,
|
||||
{String name = "attachment", String method="POST", Map<String, String>? fields}) async {
|
||||
{String name = "attachment", String method="POST", Map<String, dynamic>? fields}) async {
|
||||
var _url = makeApiUrl(url);
|
||||
|
||||
var request = http.MultipartRequest(method, Uri.parse(_url));
|
||||
@ -569,8 +605,13 @@ class InvenTreeAPI {
|
||||
request.headers.addAll(defaultHeaders());
|
||||
|
||||
if (fields != null) {
|
||||
fields.forEach((String key, String value) {
|
||||
request.fields[key] = value;
|
||||
fields.forEach((String key, dynamic value) {
|
||||
|
||||
if (value == null) {
|
||||
request.fields[key] = "";
|
||||
} else {
|
||||
request.fields[key] = value.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -652,9 +693,9 @@ class InvenTreeAPI {
|
||||
|
||||
if (request == null) {
|
||||
// Return an "invalid" APIResponse
|
||||
return new APIResponse(
|
||||
return APIResponse(
|
||||
url: url,
|
||||
method: 'OPTIONS'
|
||||
method: "OPTIONS"
|
||||
);
|
||||
}
|
||||
|
||||
@ -671,9 +712,9 @@ class InvenTreeAPI {
|
||||
|
||||
if (request == null) {
|
||||
// Return an "invalid" APIResponse
|
||||
return new APIResponse(
|
||||
return APIResponse(
|
||||
url: url,
|
||||
method: 'POST'
|
||||
method: "POST"
|
||||
);
|
||||
}
|
||||
|
||||
@ -684,15 +725,13 @@ class InvenTreeAPI {
|
||||
);
|
||||
}
|
||||
|
||||
HttpClient createClient(bool allowBadCert) {
|
||||
HttpClient createClient({bool allowBadCert = true}) {
|
||||
|
||||
var client = new HttpClient();
|
||||
var client = HttpClient();
|
||||
|
||||
client.badCertificateCallback = ((X509Certificate cert, String host, int port) {
|
||||
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
|
||||
// TODO - Introspection of actual certificate?
|
||||
|
||||
allowBadCert = true;
|
||||
|
||||
if (allowBadCert) {
|
||||
return true;
|
||||
} else {
|
||||
@ -702,7 +741,7 @@ class InvenTreeAPI {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Set the connection timeout
|
||||
client.connectionTimeout = Duration(seconds: 30);
|
||||
@ -714,7 +753,7 @@ class InvenTreeAPI {
|
||||
* Initiate a HTTP request to the server
|
||||
*
|
||||
* @param url is the API endpoint
|
||||
* @param method is the HTTP method e.g. 'POST' / 'PATCH' / 'GET' etc;
|
||||
* @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
|
||||
* @param params is the request parameters
|
||||
*/
|
||||
Future<HttpClientRequest?> apiRequest(String url, String method, {Map<String, String> urlParams = const {}}) async {
|
||||
@ -731,7 +770,7 @@ class InvenTreeAPI {
|
||||
}
|
||||
|
||||
// Remove extraneous character if present
|
||||
if (_url.endsWith('&')) {
|
||||
if (_url.endsWith("&")) {
|
||||
_url = _url.substring(0, _url.length - 1);
|
||||
}
|
||||
|
||||
@ -749,7 +788,7 @@ class InvenTreeAPI {
|
||||
|
||||
HttpClientRequest? _request;
|
||||
|
||||
var client = createClient(true);
|
||||
var client = createClient(allowBadCert: true);
|
||||
|
||||
// Attempt to open a connection to the server
|
||||
try {
|
||||
@ -757,8 +796,8 @@ class InvenTreeAPI {
|
||||
|
||||
// Set headers
|
||||
_request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
|
||||
_request.headers.set(HttpHeaders.acceptHeader, 'application/json');
|
||||
_request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
|
||||
_request.headers.set(HttpHeaders.acceptHeader, "application/json");
|
||||
_request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
|
||||
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
|
||||
|
||||
return _request;
|
||||
@ -792,7 +831,7 @@ class InvenTreeAPI {
|
||||
request.add(encoded_data);
|
||||
}
|
||||
|
||||
APIResponse response = new APIResponse(
|
||||
APIResponse response = APIResponse(
|
||||
method: request.method,
|
||||
url: request.uri.toString()
|
||||
);
|
||||
@ -805,6 +844,19 @@ class InvenTreeAPI {
|
||||
// If the server returns a server error code, alert the user
|
||||
if (_response.statusCode >= 500) {
|
||||
showStatusCodeError(_response.statusCode);
|
||||
|
||||
sentryReportMessage(
|
||||
"Server error",
|
||||
context: {
|
||||
"url": request.uri.toString(),
|
||||
"method": request.method,
|
||||
"statusCode": _response.statusCode.toString(),
|
||||
"requestHeaders": request.headers.toString(),
|
||||
"responseHeaders": _response.headers.toString(),
|
||||
"responseData": response.data.toString(),
|
||||
}
|
||||
);
|
||||
|
||||
} else {
|
||||
response.data = await responseToJson(_response) ?? {};
|
||||
|
||||
@ -814,21 +866,6 @@ class InvenTreeAPI {
|
||||
if (statusCode != _response.statusCode) {
|
||||
showStatusCodeError(_response.statusCode);
|
||||
}
|
||||
|
||||
// Report any server errors
|
||||
if (_response.statusCode >= 500) {
|
||||
sentryReportMessage(
|
||||
"Server error",
|
||||
context: {
|
||||
"url": request.uri.toString(),
|
||||
"method": request.method,
|
||||
"statusCode": _response.statusCode.toString(),
|
||||
"requestHeaders": request.headers.toString(),
|
||||
"responseHeaders": _response.headers.toString(),
|
||||
"responseData": response.data.toString(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -898,9 +935,9 @@ class InvenTreeAPI {
|
||||
|
||||
if (request == null) {
|
||||
// Return an "invalid" APIResponse
|
||||
return new APIResponse(
|
||||
return APIResponse(
|
||||
url: url,
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
error: "HttpClientRequest is null",
|
||||
);
|
||||
}
|
||||
@ -910,11 +947,11 @@ class InvenTreeAPI {
|
||||
|
||||
// Return a list of request headers
|
||||
Map<String, String> defaultHeaders() {
|
||||
var headers = Map<String, String>();
|
||||
Map<String, String> headers = {};
|
||||
|
||||
headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
|
||||
headers[HttpHeaders.acceptHeader] = 'application/json';
|
||||
headers[HttpHeaders.contentTypeHeader] = 'application/json';
|
||||
headers[HttpHeaders.acceptHeader] = "application/json";
|
||||
headers[HttpHeaders.contentTypeHeader] = "application/json";
|
||||
headers[HttpHeaders.acceptLanguageHeader] = Intl.getCurrentLocale();
|
||||
|
||||
return headers;
|
||||
@ -924,7 +961,7 @@ class InvenTreeAPI {
|
||||
if (_token.isNotEmpty) {
|
||||
return "Token $_token";
|
||||
} else if (profile != null) {
|
||||
return "Basic " + base64Encode(utf8.encode('${profile?.username}:${profile?.password}'));
|
||||
return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}"));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
@ -954,10 +991,10 @@ class InvenTreeAPI {
|
||||
)
|
||||
);
|
||||
|
||||
return new CachedNetworkImage(
|
||||
return CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
placeholder: (context, url) => CircularProgressIndicator(),
|
||||
errorWidget: (context, url, error) => Icon(FontAwesomeIcons.exclamation),
|
||||
errorWidget: (context, url, error) => FaIcon(FontAwesomeIcons.timesCircle, color: COLOR_DANGER),
|
||||
httpHeaders: defaultHeaders(),
|
||||
height: height,
|
||||
width: width,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import 'dart:ui';
|
||||
import "dart:ui";
|
||||
|
||||
const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
|
||||
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
|
||||
|
@ -2,14 +2,20 @@
|
||||
* Class for managing app-level configuration options
|
||||
*/
|
||||
|
||||
import 'package:sembast/sembast.dart';
|
||||
import 'package:inventree/preferences.dart';
|
||||
import "package:sembast/sembast.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
class InvenTreeSettingsManager {
|
||||
|
||||
factory InvenTreeSettingsManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
InvenTreeSettingsManager._internal();
|
||||
|
||||
final store = StoreRef("settings");
|
||||
|
||||
Future<Database> get _db async => await InvenTreePreferencesDB.instance.database;
|
||||
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
|
||||
|
||||
Future<dynamic> getValue(String key, dynamic backup) async {
|
||||
|
||||
@ -22,17 +28,22 @@ class InvenTreeSettingsManager {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Load a boolean setting
|
||||
Future<bool> getBool(String key, bool backup) async {
|
||||
final dynamic value = await getValue(key, backup);
|
||||
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else {
|
||||
return backup;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setValue(String key, dynamic value) async {
|
||||
|
||||
await store.record(key).put(await _db, value);
|
||||
}
|
||||
|
||||
// Ensure we only ever create a single instance of this class
|
||||
static final InvenTreeSettingsManager _manager = new InvenTreeSettingsManager._internal();
|
||||
|
||||
factory InvenTreeSettingsManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
InvenTreeSettingsManager._internal();
|
||||
}
|
||||
static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal();
|
||||
}
|
||||
|
258
lib/barcode.dart
258
lib/barcode.dart
@ -1,26 +1,24 @@
|
||||
import 'package:inventree/app_settings.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:one_context/one_context.dart';
|
||||
import "dart:io";
|
||||
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:qr_code_scanner/qr_code_scanner.dart";
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
import 'package:inventree/widget/location_display.dart';
|
||||
import 'package:inventree/widget/part_detail.dart';
|
||||
import 'package:inventree/widget/stock_detail.dart';
|
||||
|
||||
import 'dart:io';
|
||||
import "package:inventree/widget/location_display.dart";
|
||||
import "package:inventree/widget/part_detail.dart";
|
||||
import "package:inventree/widget/stock_detail.dart";
|
||||
|
||||
|
||||
class BarcodeHandler {
|
||||
@ -32,31 +30,11 @@ class BarcodeHandler {
|
||||
* based on the response returned from the InvenTree server
|
||||
*/
|
||||
|
||||
String getOverlayText(BuildContext context) => "Barcode Overlay";
|
||||
BarcodeHandler();
|
||||
|
||||
BarcodeHandler();
|
||||
String getOverlayText(BuildContext context) => "Barcode Overlay";
|
||||
|
||||
QRViewController? _controller;
|
||||
|
||||
void successTone() async {
|
||||
|
||||
final bool en = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
|
||||
|
||||
if (en) {
|
||||
final player = AudioCache();
|
||||
player.play("sounds/barcode_scan.mp3");
|
||||
}
|
||||
}
|
||||
|
||||
void failureTone() async {
|
||||
|
||||
final bool en = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
|
||||
|
||||
if (en) {
|
||||
final player = AudioCache();
|
||||
player.play("sounds/barcode_error.mp3");
|
||||
}
|
||||
}
|
||||
QRViewController? _controller;
|
||||
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
// Called when the server "matches" a barcode
|
||||
@ -101,8 +79,10 @@ class BarcodeHandler {
|
||||
|
||||
_controller?.resumeCamera();
|
||||
|
||||
Map<String, dynamic> data = response.asMap();
|
||||
|
||||
// Handle strange response from the server
|
||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
||||
if (!response.isValid() || !response.isMap()) {
|
||||
onBarcodeUnknown(context, {});
|
||||
|
||||
// We want to know about this one!
|
||||
@ -118,12 +98,12 @@ class BarcodeHandler {
|
||||
"errorDetail": response.errorDetail,
|
||||
}
|
||||
);
|
||||
} else if (response.data.containsKey('error')) {
|
||||
onBarcodeUnknown(context, response.data);
|
||||
} else if (response.data.containsKey('success')) {
|
||||
onBarcodeMatched(context, response.data);
|
||||
} else if (data.containsKey("error")) {
|
||||
onBarcodeUnknown(context, data);
|
||||
} else if (data.containsKey("success")) {
|
||||
onBarcodeMatched(context, data);
|
||||
} else {
|
||||
onBarcodeUnhandled(context, response.data);
|
||||
onBarcodeUnhandled(context, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,9 +136,9 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
int pk = -1;
|
||||
|
||||
// A stocklocation has been passed?
|
||||
if (data.containsKey('stocklocation')) {
|
||||
if (data.containsKey("stocklocation")) {
|
||||
|
||||
pk = (data['stocklocation']?['pk'] ?? -1) as int;
|
||||
pk = (data["stocklocation"]?["pk"] ?? -1) as int;
|
||||
|
||||
if (pk > 0) {
|
||||
|
||||
@ -180,9 +160,9 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
);
|
||||
}
|
||||
|
||||
} else if (data.containsKey('stockitem')) {
|
||||
} else if (data.containsKey("stockitem")) {
|
||||
|
||||
pk = (data['stockitem']?['pk'] ?? -1) as int;
|
||||
pk = (data["stockitem"]?["pk"] ?? -1) as int;
|
||||
|
||||
if (pk > 0) {
|
||||
|
||||
@ -206,9 +186,9 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
success: false
|
||||
);
|
||||
}
|
||||
} else if (data.containsKey('part')) {
|
||||
} else if (data.containsKey("part")) {
|
||||
|
||||
pk = (data['part']?['pk'] ?? -1) as int;
|
||||
pk = (data["part"]?["pk"] ?? -1) as int;
|
||||
|
||||
if (pk > 0) {
|
||||
|
||||
@ -258,93 +238,24 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Barcode handler for assigning a new barcode to a stock item
|
||||
*/
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
StockItemBarcodeAssignmentHandler(this.item);
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
|
||||
failureTone();
|
||||
|
||||
// If the barcode is known, we can't assign it to the stock item!
|
||||
showSnackIcon(
|
||||
L10().barcodeInUse,
|
||||
icon: FontAwesomeIcons.qrcode,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
||||
// If the barcode is unknown, we *can* assign it to the stock item!
|
||||
|
||||
if (!data.containsKey("hash")) {
|
||||
showServerError(
|
||||
L10().missingData,
|
||||
L10().barcodeMissingHash,
|
||||
);
|
||||
} else {
|
||||
|
||||
// Send the 'hash' code as the UID for the stock item
|
||||
item.update(
|
||||
values: {
|
||||
"uid": data['hash'],
|
||||
}
|
||||
).then((result) {
|
||||
if (result) {
|
||||
|
||||
failureTone();
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeAssigned,
|
||||
success: true,
|
||||
icon: FontAwesomeIcons.qrcode
|
||||
);
|
||||
} else {
|
||||
|
||||
successTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeNotAssigned,
|
||||
success: false,
|
||||
icon: FontAwesomeIcons.qrcode
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StockItemScanIntoLocationHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Barcode handler for scanning a provided StockItem into a scanned StockLocation
|
||||
*/
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
StockItemScanIntoLocationHandler(this.item);
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
// If the barcode points to a 'stocklocation', great!
|
||||
if (data.containsKey('stocklocation')) {
|
||||
// If the barcode points to a "stocklocation", great!
|
||||
if (data.containsKey("stocklocation")) {
|
||||
// Extract location information
|
||||
int location = (data['stocklocation']['pk'] ?? -1) as int;
|
||||
int location = (data["stocklocation"]["pk"] ?? -1) as int;
|
||||
|
||||
if (location == -1) {
|
||||
showSnackIcon(
|
||||
@ -394,11 +305,11 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Barcode handler for scanning stock item(s) into the specified StockLocation
|
||||
*/
|
||||
|
||||
final InvenTreeStockLocation location;
|
||||
|
||||
|
||||
StockLocationScanInItemsHandler(this.location);
|
||||
|
||||
|
||||
final InvenTreeStockLocation location;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
||||
|
||||
@ -406,11 +317,11 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
|
||||
// Returned barcode must match a stock item
|
||||
if (data.containsKey('stockitem')) {
|
||||
if (data.containsKey("stockitem")) {
|
||||
|
||||
int item_id = data['stockitem']['pk'] as int;
|
||||
int item_id = data["stockitem"]["pk"] as int;
|
||||
|
||||
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem;
|
||||
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem?;
|
||||
|
||||
if (item == null) {
|
||||
|
||||
@ -462,11 +373,78 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
|
||||
}
|
||||
|
||||
|
||||
class UniqueBarcodeHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
||||
*/
|
||||
|
||||
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
|
||||
|
||||
// Callback function when a "unique" barcode hash is found
|
||||
final Function(String) callback;
|
||||
|
||||
final String overlayText;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) {
|
||||
if (overlayText.isEmpty) {
|
||||
return L10().barcodeScanAssign;
|
||||
} else {
|
||||
return overlayText;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
|
||||
failureTone();
|
||||
|
||||
// If the barcode is known, we can"t assign it to the stock item!
|
||||
showSnackIcon(
|
||||
L10().barcodeInUse,
|
||||
icon: FontAwesomeIcons.qrcode,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
||||
// If the barcode is unknown, we *can* assign it to the stock item!
|
||||
|
||||
if (!data.containsKey("hash")) {
|
||||
showServerError(
|
||||
L10().missingData,
|
||||
L10().barcodeMissingHash,
|
||||
);
|
||||
} else {
|
||||
String hash = (data["hash"] ?? "") as String;
|
||||
|
||||
if (hash.isEmpty) {
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeError,
|
||||
success: false,
|
||||
);
|
||||
} else {
|
||||
|
||||
successTone();
|
||||
|
||||
// Close the barcode scanner
|
||||
Navigator.of(context).pop();
|
||||
|
||||
callback(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class InvenTreeQRView extends StatefulWidget {
|
||||
|
||||
final BarcodeHandler _handler;
|
||||
const InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
|
||||
|
||||
InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
|
||||
final BarcodeHandler _handler;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _QRViewState(_handler);
|
||||
@ -475,7 +453,9 @@ class InvenTreeQRView extends StatefulWidget {
|
||||
|
||||
class _QRViewState extends State<InvenTreeQRView> {
|
||||
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
_QRViewState(this._handler) : super();
|
||||
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: "QR");
|
||||
|
||||
QRViewController? _controller;
|
||||
|
||||
@ -494,8 +474,6 @@ class _QRViewState extends State<InvenTreeQRView> {
|
||||
_controller!.resumeCamera();
|
||||
}
|
||||
|
||||
_QRViewState(this._handler) : super();
|
||||
|
||||
void _onViewCreated(BuildContext context, QRViewController controller) {
|
||||
_controller = controller;
|
||||
controller.scannedDataStream.listen((barcode) {
|
||||
|
3
lib/dummy_dsn.dart
Normal file
3
lib/dummy_dsn.dart
Normal 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";
|
@ -12,11 +12,9 @@ import 'package:flutter/material.dart';
|
||||
class S implements WidgetsLocalizations {
|
||||
const S();
|
||||
|
||||
static const GeneratedLocalizationsDelegate delegate =
|
||||
const GeneratedLocalizationsDelegate();
|
||||
static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate();
|
||||
|
||||
static S of(BuildContext context) =>
|
||||
Localizations.of<S>(context, WidgetsLocalizations);
|
||||
static S of(BuildContext context) => Localizations.of<S>(context, WidgetsLocalizations);
|
||||
|
||||
@override
|
||||
TextDirection get textDirection => TextDirection.ltr;
|
||||
|
37
lib/helpers.dart
Normal file
37
lib/helpers.dart
Normal 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");
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import 'package:inventree/api.dart';
|
||||
import "dart:async";
|
||||
|
||||
import 'model.dart';
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/purchase_order.dart";
|
||||
|
||||
|
||||
/*
|
||||
@ -9,6 +11,10 @@ import 'model.dart';
|
||||
|
||||
class InvenTreeCompany extends InvenTreeModel {
|
||||
|
||||
InvenTreeCompany() : super();
|
||||
|
||||
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "company/";
|
||||
|
||||
@ -25,25 +31,51 @@ class InvenTreeCompany extends InvenTreeModel {
|
||||
};
|
||||
}
|
||||
|
||||
InvenTreeCompany() : super();
|
||||
String get image => (jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage) as String;
|
||||
|
||||
String get image => jsondata['image'] ?? jsondata['thumbnail'] ?? InvenTreeAPI.staticImage;
|
||||
String get thumbnail => (jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb) as String;
|
||||
|
||||
String get thumbnail => jsondata['thumbnail'] ?? jsondata['image'] ?? InvenTreeAPI.staticThumb;
|
||||
String get website => (jsondata["website"] ?? "") as String;
|
||||
|
||||
String get website => jsondata['website'] ?? '';
|
||||
String get phone => (jsondata["phone"] ?? "") as String;
|
||||
|
||||
String get phone => jsondata['phone'] ?? '';
|
||||
String get email => (jsondata["email"] ?? "") as String;
|
||||
|
||||
String get email => jsondata['email'] ?? '';
|
||||
bool get isSupplier => (jsondata["is_supplier"] ?? false) as bool;
|
||||
|
||||
bool get isSupplier => jsondata['is_supplier'] ?? false;
|
||||
bool get isManufacturer => (jsondata["is_manufacturer"] ?? false) as bool;
|
||||
|
||||
bool get isManufacturer => jsondata['is_manufacturer'] ?? false;
|
||||
bool get isCustomer => (jsondata["is_customer"] ?? false) as bool;
|
||||
|
||||
bool get isCustomer => jsondata['is_customer'] ?? false;
|
||||
int get partSuppliedCount => (jsondata["parts_supplied"] ?? 0) as int;
|
||||
|
||||
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
int get partManufacturedCount => (jsondata["parts_manufactured"] ?? 0) as int;
|
||||
|
||||
// Request a list of purchase orders against this company
|
||||
Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({bool? outstanding}) async {
|
||||
|
||||
Map<String, String> filters = {
|
||||
"supplier": "${pk}"
|
||||
};
|
||||
|
||||
if (outstanding != null) {
|
||||
filters["outstanding"] = outstanding ? "true" : "false";
|
||||
}
|
||||
|
||||
final List<InvenTreeModel> results = await InvenTreePurchaseOrder().list(
|
||||
filters: filters
|
||||
);
|
||||
|
||||
List<InvenTreePurchaseOrder> orders = [];
|
||||
|
||||
for (InvenTreeModel model in results) {
|
||||
if (model is InvenTreePurchaseOrder) {
|
||||
orders.add(model);
|
||||
}
|
||||
}
|
||||
|
||||
return orders;
|
||||
}
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
@ -58,6 +90,11 @@ class InvenTreeCompany extends InvenTreeModel {
|
||||
* The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
|
||||
*/
|
||||
class InvenTreeSupplierPart extends InvenTreeModel {
|
||||
|
||||
InvenTreeSupplierPart() : super();
|
||||
|
||||
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "company/part/";
|
||||
|
||||
@ -79,27 +116,29 @@ class InvenTreeSupplierPart extends InvenTreeModel {
|
||||
return _filters();
|
||||
}
|
||||
|
||||
InvenTreeSupplierPart() : super();
|
||||
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
|
||||
|
||||
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
String get manufacturerName => (jsondata["manufacturer_detail"]["name"] ?? "") as String;
|
||||
|
||||
int get manufacturerId => (jsondata['manufacturer'] ?? -1) as int;
|
||||
String get manufacturerImage => (jsondata["manufacturer_detail"]["image"] ?? jsondata["manufacturer_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||
|
||||
String get manufacturerName => jsondata['manufacturer_detail']['name'];
|
||||
int get manufacturerPartId => (jsondata["manufacturer_part"] ?? -1) as int;
|
||||
|
||||
String get manufacturerImage => jsondata['manufacturer_detail']['image'] ?? jsondata['manufacturer_detail']['thumbnail'];
|
||||
int get supplierId => (jsondata["supplier"] ?? -1) as int;
|
||||
|
||||
int get manufacturerPartId => (jsondata['manufacturer_part'] ?? -1) as int;
|
||||
String get supplierName => (jsondata["supplier_detail"]["name"] ?? "") as String;
|
||||
|
||||
int get supplierId => (jsondata['supplier'] ?? -1) as int;
|
||||
String get supplierImage => (jsondata["supplier_detail"]["image"] ?? jsondata["supplier_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||
|
||||
String get supplierName => jsondata['supplier_detail']['name'];
|
||||
String get SKU => (jsondata["SKU"] ?? "") as String;
|
||||
|
||||
String get supplierImage => jsondata['supplier_detail']['image'] ?? jsondata['supplier_detail']['thumbnail'];
|
||||
String get MPN => (jsondata["MPN"] ?? "") as String;
|
||||
|
||||
String get SKU => (jsondata['SKU'] ?? '') as String;
|
||||
int get partId => (jsondata["part"] ?? -1) as int;
|
||||
|
||||
String get MPN => jsondata['MPN'] ?? '';
|
||||
String get partImage => (jsondata["part_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||
|
||||
String get partName => (jsondata["part_detail"]["full_name"] ?? "") as String;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
@ -112,6 +151,10 @@ class InvenTreeSupplierPart extends InvenTreeModel {
|
||||
|
||||
class InvenTreeManufacturerPart extends InvenTreeModel {
|
||||
|
||||
InvenTreeManufacturerPart() : super();
|
||||
|
||||
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String url = "company/part/manufacturer/";
|
||||
|
||||
@ -122,15 +165,11 @@ class InvenTreeManufacturerPart extends InvenTreeModel {
|
||||
};
|
||||
}
|
||||
|
||||
InvenTreeManufacturerPart() : super();
|
||||
int get partId => (jsondata["part"] ?? -1) as int;
|
||||
|
||||
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
|
||||
|
||||
int get partId => (jsondata['part'] ?? -1) as int;
|
||||
|
||||
int get manufacturerId => (jsondata['manufacturer'] ?? -1) as int;
|
||||
|
||||
String get MPN => (jsondata['MPN'] ?? '') as String;
|
||||
String get MPN => (jsondata["MPN"] ?? "") as String;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
|
@ -1,18 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:http/http.dart' as http;
|
||||
import "package:path/path.dart" as path;
|
||||
|
||||
import '../l10.dart';
|
||||
import '../api_form.dart';
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/api_form.dart";
|
||||
|
||||
|
||||
// Paginated response object
|
||||
@ -40,12 +39,17 @@ class InvenTreePageResponse {
|
||||
*/
|
||||
class InvenTreeModel {
|
||||
|
||||
InvenTreeModel();
|
||||
|
||||
// Construct an InvenTreeModel from a JSON data object
|
||||
InvenTreeModel.fromJson(this.jsondata);
|
||||
|
||||
// Override the endpoint URL for each subclass
|
||||
String get URL => "";
|
||||
|
||||
// Override the web URL for each subclass
|
||||
// Note: If the WEB_URL is the same (except for /api/) as URL then just leave blank
|
||||
String WEB_URL = "";
|
||||
String get WEB_URL => "";
|
||||
|
||||
String get webUrl {
|
||||
|
||||
@ -114,36 +118,23 @@ class InvenTreeModel {
|
||||
Map<String, dynamic> jsondata = {};
|
||||
|
||||
// Accessor for the API
|
||||
var api = InvenTreeAPI();
|
||||
InvenTreeAPI get api => InvenTreeAPI();
|
||||
|
||||
// Default empty object constructor
|
||||
InvenTreeModel() {
|
||||
jsondata.clear();
|
||||
}
|
||||
|
||||
// Construct an InvenTreeModel from a JSON data object
|
||||
InvenTreeModel.fromJson(Map<String, dynamic> json) {
|
||||
|
||||
// Store the json object
|
||||
jsondata = json;
|
||||
|
||||
}
|
||||
|
||||
int get pk => (jsondata['pk'] ?? -1) as int;
|
||||
int get pk => (jsondata["pk"] ?? -1) as int;
|
||||
|
||||
// Some common accessors
|
||||
String get name => jsondata['name'] ?? '';
|
||||
String get name => (jsondata["name"] ?? "") as String;
|
||||
|
||||
String get description => jsondata['description'] ?? '';
|
||||
String get description => (jsondata["description"] ?? "") as String;
|
||||
|
||||
String get notes => jsondata['notes'] ?? '';
|
||||
String get notes => (jsondata["notes"] ?? "") as String;
|
||||
|
||||
int get parentId => (jsondata['parent'] ?? -1) as int;
|
||||
int get parentId => (jsondata["parent"] ?? -1) as int;
|
||||
|
||||
// Legacy API provided external link as "URL", while newer API uses "link"
|
||||
String get link => jsondata['link'] ?? jsondata['URL'] ?? '';
|
||||
String get link => (jsondata["link"] ?? jsondata["URL"] ?? "") as String;
|
||||
|
||||
void goToInvenTreePage() async {
|
||||
Future <void> goToInvenTreePage() async {
|
||||
|
||||
if (await canLaunch(webUrl)) {
|
||||
await launch(webUrl);
|
||||
@ -152,7 +143,7 @@ class InvenTreeModel {
|
||||
}
|
||||
}
|
||||
|
||||
void openLink() async {
|
||||
Future <void> openLink() async {
|
||||
|
||||
if (link.isNotEmpty) {
|
||||
|
||||
@ -162,7 +153,7 @@ class InvenTreeModel {
|
||||
}
|
||||
}
|
||||
|
||||
String get keywords => jsondata['keywords'] ?? '';
|
||||
String get keywords => (jsondata["keywords"] ?? "") as String;
|
||||
|
||||
// Create a new object from JSON data (not a constructor!)
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
@ -176,20 +167,60 @@ class InvenTreeModel {
|
||||
String get url => "${URL}/${pk}/".replaceAll("//", "/");
|
||||
|
||||
// Search this Model type in the database
|
||||
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters = const {}}) async {
|
||||
Future<List<InvenTreeModel>> search(String searchTerm, {Map<String, String> filters = const {}, int offset = 0, int limit = 25}) async {
|
||||
|
||||
filters["search"] = searchTerm;
|
||||
Map<String, String> searchFilters = {};
|
||||
|
||||
final results = list(filters: filters);
|
||||
for (String key in filters.keys) {
|
||||
searchFilters[key] = filters[key] ?? "";
|
||||
}
|
||||
|
||||
searchFilters["search"] = searchTerm;
|
||||
searchFilters["offset"] = "${offset}";
|
||||
searchFilters["limit"] = "${limit}";
|
||||
|
||||
final results = list(filters: searchFilters);
|
||||
|
||||
return results;
|
||||
|
||||
}
|
||||
|
||||
Map<String, String> defaultListFilters() { return Map<String, String>(); }
|
||||
// Return the number of results that would meet a particular "query"
|
||||
Future<int> count({Map<String, String> filters = const {}, String searchQuery = ""} ) async {
|
||||
|
||||
var params = defaultListFilters();
|
||||
|
||||
filters.forEach((String key, String value) {
|
||||
params[key] = value;
|
||||
});
|
||||
|
||||
if (searchQuery.isNotEmpty) {
|
||||
params["search"] = searchQuery;
|
||||
}
|
||||
|
||||
// Limit to 1 result, for quick DB access
|
||||
params["limit"] = "1";
|
||||
|
||||
var response = await api.get(URL, params: params);
|
||||
|
||||
if (response.isValid()) {
|
||||
int n = int.tryParse(response.data["count"].toString()) ?? 0;
|
||||
|
||||
print("${URL} -> ${n} results");
|
||||
return n;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> defaultListFilters() {
|
||||
return {};
|
||||
}
|
||||
|
||||
// A map of "default" headers to use when performing a GET request
|
||||
Map<String, String> defaultGetFilters() { return Map<String, String>(); }
|
||||
Map<String, String> defaultGetFilters() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/*
|
||||
* Reload this object, by requesting data from the server
|
||||
@ -198,7 +229,7 @@ class InvenTreeModel {
|
||||
|
||||
var response = await api.get(url, params: defaultGetFilters(), expectedStatusCode: 200);
|
||||
|
||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
||||
if (!response.isValid() || response.data == null || (response.data is! Map)) {
|
||||
|
||||
// Report error
|
||||
if (response.statusCode > 0) {
|
||||
@ -224,7 +255,7 @@ class InvenTreeModel {
|
||||
|
||||
}
|
||||
|
||||
jsondata = response.data;
|
||||
jsondata = response.asMap();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -267,12 +298,12 @@ class InvenTreeModel {
|
||||
|
||||
// Override any default values
|
||||
for (String key in filters.keys) {
|
||||
params[key] = filters[key] ?? '';
|
||||
params[key] = filters[key] ?? "";
|
||||
}
|
||||
|
||||
var response = await api.get(url, params: params);
|
||||
|
||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
||||
if (!response.isValid() || response.data == null || response.data is! Map) {
|
||||
|
||||
if (response.statusCode > 0) {
|
||||
await sentryReportMessage(
|
||||
@ -297,25 +328,23 @@ class InvenTreeModel {
|
||||
|
||||
}
|
||||
|
||||
return createFromJson(response.data);
|
||||
return createFromJson(response.asMap());
|
||||
}
|
||||
|
||||
Future<InvenTreeModel?> create(Map<String, dynamic> data) async {
|
||||
|
||||
print("CREATE: ${URL} ${data.toString()}");
|
||||
|
||||
if (data.containsKey('pk')) {
|
||||
data.remove('pk');
|
||||
if (data.containsKey("pk")) {
|
||||
data.remove("pk");
|
||||
}
|
||||
|
||||
if (data.containsKey('id')) {
|
||||
data.remove('id');
|
||||
if (data.containsKey("id")) {
|
||||
data.remove("id");
|
||||
}
|
||||
|
||||
var response = await api.post(URL, body: data);
|
||||
|
||||
// Invalid response returned from server
|
||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
||||
if (!response.isValid() || response.data == null || response.data is! Map) {
|
||||
|
||||
if (response.statusCode > 0) {
|
||||
await sentryReportMessage(
|
||||
@ -340,19 +369,34 @@ class InvenTreeModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createFromJson(response.data);
|
||||
return createFromJson(response.asMap());
|
||||
}
|
||||
|
||||
Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async {
|
||||
var params = defaultListFilters();
|
||||
|
||||
for (String key in filters.keys) {
|
||||
params[key] = filters[key] ?? '';
|
||||
params[key] = filters[key] ?? "";
|
||||
}
|
||||
|
||||
params["limit"] = "${limit}";
|
||||
params["offset"] = "${offset}";
|
||||
|
||||
/* Special case: "original_search":
|
||||
* - We may wish to provide an original "query" which is augmented by the user
|
||||
* - Thus, "search" and "original_search" may both be provided
|
||||
* - In such a case, we want to concatenate them together
|
||||
*/
|
||||
if (params.containsKey("original_search")) {
|
||||
|
||||
String search = params["search"] ?? "";
|
||||
String original = params["original_search"] ?? "";
|
||||
|
||||
params["search"] = "${search} ${original}".trim();
|
||||
|
||||
params.remove("original_search");
|
||||
}
|
||||
|
||||
var response = await api.get(URL, params: params);
|
||||
|
||||
if (!response.isValid()) {
|
||||
@ -360,15 +404,17 @@ class InvenTreeModel {
|
||||
}
|
||||
|
||||
// Construct the response
|
||||
InvenTreePageResponse page = new InvenTreePageResponse();
|
||||
InvenTreePageResponse page = InvenTreePageResponse();
|
||||
|
||||
if (response.data.containsKey("count") && response.data.containsKey("results")) {
|
||||
page.count = response.data["count"] as int;
|
||||
var data = response.asMap();
|
||||
|
||||
if (data.containsKey("count") && data.containsKey("results")) {
|
||||
page.count = (data["count"] ?? 0) as int;
|
||||
|
||||
page.results = [];
|
||||
|
||||
for (var result in response.data["results"]) {
|
||||
page.addResult(createFromJson(result));
|
||||
page.addResult(createFromJson(result as Map<String, dynamic>));
|
||||
}
|
||||
|
||||
return page;
|
||||
@ -384,7 +430,7 @@ class InvenTreeModel {
|
||||
var params = defaultListFilters();
|
||||
|
||||
for (String key in filters.keys) {
|
||||
params[key] = filters[key] ?? '';
|
||||
params[key] = filters[key] ?? "";
|
||||
}
|
||||
|
||||
var response = await api.get(URL, params: params);
|
||||
@ -396,20 +442,22 @@ class InvenTreeModel {
|
||||
return results;
|
||||
}
|
||||
|
||||
dynamic data;
|
||||
List<dynamic> data = [];
|
||||
|
||||
if (response.data is List) {
|
||||
data = response.data;
|
||||
} else if (response.data.containsKey('results')) {
|
||||
data = response.data['results'];
|
||||
} else {
|
||||
data = [];
|
||||
if (response.isList()) {
|
||||
data = response.asList();
|
||||
} else if (response.isMap()) {
|
||||
var mData = response.asMap();
|
||||
|
||||
if (mData.containsKey("results")) {
|
||||
data = (response.data["results"] ?? []) as List<dynamic>;
|
||||
}
|
||||
}
|
||||
|
||||
for (var d in data) {
|
||||
|
||||
// Create a new object (of the current class type
|
||||
InvenTreeModel obj = createFromJson(d);
|
||||
InvenTreeModel obj = createFromJson(d as Map<String, dynamic>);
|
||||
|
||||
results.add(obj);
|
||||
}
|
||||
@ -421,9 +469,9 @@ class InvenTreeModel {
|
||||
// Provide a listing of objects at the endpoint
|
||||
// TODO - Static function which returns a list of objects (of this class)
|
||||
|
||||
// TODO - Define a 'delete' function
|
||||
// TODO - Define a "delete" function
|
||||
|
||||
// TODO - Define a 'save' / 'update' function
|
||||
// TODO - Define a "save" / "update" function
|
||||
|
||||
// Override this function for each sub-class
|
||||
bool matchAgainstString(String filter) {
|
||||
@ -457,10 +505,11 @@ class InvenTreeModel {
|
||||
|
||||
class InvenTreeAttachment extends InvenTreeModel {
|
||||
// Class representing an "attachment" file
|
||||
|
||||
InvenTreeAttachment() : super();
|
||||
|
||||
String get attachment => jsondata["attachment"] ?? '';
|
||||
InvenTreeAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
String get attachment => (jsondata["attachment"] ?? "") as String;
|
||||
|
||||
// Return the filename of the attachment
|
||||
String get filename {
|
||||
@ -498,25 +547,23 @@ class InvenTreeAttachment extends InvenTreeModel {
|
||||
return FontAwesomeIcons.fileAlt;
|
||||
}
|
||||
|
||||
String get comment => jsondata["comment"] ?? '';
|
||||
String get comment => (jsondata["comment"] ?? "") as String;
|
||||
|
||||
DateTime? get uploadDate {
|
||||
if (jsondata.containsKey("upload_date")) {
|
||||
return DateTime.tryParse(jsondata["upload_date"] ?? '');
|
||||
return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
InvenTreeAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async {
|
||||
|
||||
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||
URL,
|
||||
attachment,
|
||||
method: 'POST',
|
||||
name: 'attachment',
|
||||
method: "POST",
|
||||
name: "attachment",
|
||||
fields: fields
|
||||
);
|
||||
|
||||
|
@ -1,15 +1,19 @@
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/inventree/company.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "dart:io";
|
||||
|
||||
import 'model.dart';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "model.dart";
|
||||
|
||||
class InvenTreePartCategory extends InvenTreeModel {
|
||||
|
||||
InvenTreePartCategory() : super();
|
||||
|
||||
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "part/category/";
|
||||
|
||||
@ -25,21 +29,20 @@ class InvenTreePartCategory extends InvenTreeModel {
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
var filters = new Map<String, String>();
|
||||
|
||||
filters["active"] = "true";
|
||||
filters["cascade"] = "false";
|
||||
|
||||
return filters;
|
||||
return {
|
||||
"active": "true",
|
||||
"cascade": "false"
|
||||
};
|
||||
}
|
||||
|
||||
String get pathstring => jsondata['pathstring'] ?? '';
|
||||
String get pathstring => (jsondata["pathstring"] ?? "") as String;
|
||||
|
||||
String get parentpathstring {
|
||||
// TODO - Drive the refactor tractor through this
|
||||
List<String> psplit = pathstring.split("/");
|
||||
|
||||
if (psplit.length > 0) {
|
||||
if (psplit.isNotEmpty) {
|
||||
psplit.removeLast();
|
||||
}
|
||||
|
||||
@ -52,11 +55,7 @@ class InvenTreePartCategory extends InvenTreeModel {
|
||||
return p;
|
||||
}
|
||||
|
||||
int get partcount => jsondata['parts'] ?? 0;
|
||||
|
||||
InvenTreePartCategory() : super();
|
||||
|
||||
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
int get partcount => (jsondata["parts"] ?? 0) as int;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
@ -71,25 +70,23 @@ class InvenTreePartCategory extends InvenTreeModel {
|
||||
|
||||
class InvenTreePartTestTemplate extends InvenTreeModel {
|
||||
|
||||
@override
|
||||
String get URL => "part/test-template/";
|
||||
|
||||
String get key => jsondata['key'] ?? '';
|
||||
|
||||
String get testName => jsondata['test_name'] ?? '';
|
||||
|
||||
String get description => jsondata['description'] ?? '';
|
||||
|
||||
bool get required => jsondata['required'] ?? false;
|
||||
|
||||
bool get requiresValue => jsondata['requires_value'] ?? false;
|
||||
|
||||
bool get requiresAttachment => jsondata['requires_attachment'] ?? false;
|
||||
|
||||
InvenTreePartTestTemplate() : super();
|
||||
|
||||
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "part/test-template/";
|
||||
|
||||
String get key => (jsondata["key"] ?? "") as String;
|
||||
|
||||
String get testName => (jsondata["test_name"] ?? "") as String;
|
||||
|
||||
bool get required => (jsondata["required"] ?? false) as bool;
|
||||
|
||||
bool get requiresValue => (jsondata["requires_value"] ?? false) as bool;
|
||||
|
||||
bool get requiresAttachment => (jsondata["requires_attachment"] ?? false) as bool;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
var template = InvenTreePartTestTemplate.fromJson(json);
|
||||
@ -125,6 +122,10 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
|
||||
|
||||
class InvenTreePart extends InvenTreeModel {
|
||||
|
||||
InvenTreePart() : super();
|
||||
|
||||
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "part/";
|
||||
|
||||
@ -138,9 +139,9 @@ class InvenTreePart extends InvenTreeModel {
|
||||
"keywords": {},
|
||||
"link": {},
|
||||
|
||||
// Parent category
|
||||
"category": {
|
||||
},
|
||||
"category": {},
|
||||
|
||||
"default_location": {},
|
||||
|
||||
"units": {},
|
||||
|
||||
@ -195,7 +196,7 @@ class InvenTreePart extends InvenTreeModel {
|
||||
});
|
||||
}
|
||||
|
||||
int get supplierCount => (jsondata['suppliers'] ?? 0) as int;
|
||||
int get supplierCount => (jsondata["suppliers"] ?? 0) as int;
|
||||
|
||||
// Request supplier parts for this part
|
||||
Future<List<InvenTreeSupplierPart>> getSupplierParts() async {
|
||||
@ -241,8 +242,10 @@ class InvenTreePart extends InvenTreeModel {
|
||||
});
|
||||
}
|
||||
|
||||
int? get defaultLocation => jsondata["default_location"] as int?;
|
||||
|
||||
// Get the number of stock on order for this Part
|
||||
double get onOrder => double.tryParse(jsondata['ordering'].toString()) ?? 0;
|
||||
double get onOrder => double.tryParse(jsondata["ordering"].toString()) ?? 0;
|
||||
|
||||
String get onOrderString {
|
||||
|
||||
@ -254,7 +257,7 @@ class InvenTreePart extends InvenTreeModel {
|
||||
}
|
||||
|
||||
// Get the stock count for this Part
|
||||
double get inStock => double.tryParse(jsondata['in_stock'].toString()) ?? 0;
|
||||
double get inStock => double.tryParse(jsondata["in_stock"].toString()) ?? 0;
|
||||
|
||||
String get inStockString {
|
||||
|
||||
@ -271,69 +274,69 @@ class InvenTreePart extends InvenTreeModel {
|
||||
return q;
|
||||
}
|
||||
|
||||
String get units => jsondata["units"] ?? "";
|
||||
String get units => (jsondata["units"] ?? "") as String;
|
||||
|
||||
// Get the number of units being build for this Part
|
||||
double get building => double.tryParse(jsondata['building'].toString()) ?? 0;
|
||||
double get building => double.tryParse(jsondata["building"].toString()) ?? 0;
|
||||
|
||||
// Get the number of BOM items in this Part (if it is an assembly)
|
||||
int get bomItemCount => (jsondata['bom_items'] ?? 0) as int;
|
||||
int get bomItemCount => (jsondata["bom_items"] ?? 0) as int;
|
||||
|
||||
// Get the number of BOMs this Part is used in (if it is a component)
|
||||
int get usedInCount => (jsondata['used_in'] ?? 0) as int;
|
||||
int get usedInCount => (jsondata["used_in"] ?? 0) as int;
|
||||
|
||||
bool get isAssembly => (jsondata['assembly'] ?? false) as bool;
|
||||
bool get isAssembly => (jsondata["assembly"] ?? false) as bool;
|
||||
|
||||
bool get isComponent => (jsondata['component'] ?? false) as bool;
|
||||
bool get isComponent => (jsondata["component"] ?? false) as bool;
|
||||
|
||||
bool get isPurchaseable => (jsondata['purchaseable'] ?? false) as bool;
|
||||
bool get isPurchaseable => (jsondata["purchaseable"] ?? false) as bool;
|
||||
|
||||
bool get isSalable => (jsondata['salable'] ?? false) as bool;
|
||||
bool get isSalable => (jsondata["salable"] ?? false) as bool;
|
||||
|
||||
bool get isActive => (jsondata['active'] ?? false) as bool;
|
||||
bool get isActive => (jsondata["active"] ?? false) as bool;
|
||||
|
||||
bool get isVirtual => (jsondata['virtual'] ?? false) as bool;
|
||||
bool get isVirtual => (jsondata["virtual"] ?? false) as bool;
|
||||
|
||||
bool get isTrackable => (jsondata['trackable'] ?? false) as bool;
|
||||
bool get isTrackable => (jsondata["trackable"] ?? false) as bool;
|
||||
|
||||
// Get the IPN (internal part number) for the Part instance
|
||||
String get IPN => jsondata['IPN'] ?? '';
|
||||
String get IPN => (jsondata["IPN"] ?? "") as String;
|
||||
|
||||
// Get the revision string for the Part instance
|
||||
String get revision => jsondata['revision'] ?? '';
|
||||
String get revision => (jsondata["revision"] ?? "") as String;
|
||||
|
||||
// Get the category ID for the Part instance (or 'null' if does not exist)
|
||||
int get categoryId => (jsondata['category'] ?? -1) as int;
|
||||
// Get the category ID for the Part instance (or "null" if does not exist)
|
||||
int get categoryId => (jsondata["category"] ?? -1) as int;
|
||||
|
||||
// Get the category name for the Part instance
|
||||
String get categoryName {
|
||||
// Inavlid category ID
|
||||
if (categoryId <= 0) return '';
|
||||
if (categoryId <= 0) return "";
|
||||
|
||||
if (!jsondata.containsKey('category_detail')) return '';
|
||||
if (!jsondata.containsKey("category_detail")) return "";
|
||||
|
||||
return jsondata['category_detail']?['name'] ?? '';
|
||||
return (jsondata["category_detail"]?["name"] ?? "") as String;
|
||||
}
|
||||
|
||||
// Get the category description for the Part instance
|
||||
String get categoryDescription {
|
||||
// Invalid category ID
|
||||
if (categoryId <= 0) return '';
|
||||
if (categoryId <= 0) return "";
|
||||
|
||||
if (!jsondata.containsKey('category_detail')) return '';
|
||||
if (!jsondata.containsKey("category_detail")) return "";
|
||||
|
||||
return jsondata['category_detail']?['description'] ?? '';
|
||||
return (jsondata["category_detail"]?["description"] ?? "") as String;
|
||||
}
|
||||
// Get the image URL for the Part instance
|
||||
String get _image => jsondata['image'] ?? '';
|
||||
String get _image => (jsondata["image"] ?? "") as String;
|
||||
|
||||
// Get the thumbnail URL for the Part instance
|
||||
String get _thumbnail => jsondata['thumbnail'] ?? '';
|
||||
String get _thumbnail => (jsondata["thumbnail"] ?? "") as String;
|
||||
|
||||
// Return the fully-qualified name for the Part instance
|
||||
String get fullname {
|
||||
|
||||
String fn = jsondata['full_name'] ?? '';
|
||||
String fn = (jsondata["full_name"] ?? "") as String;
|
||||
|
||||
if (fn.isNotEmpty) return fn;
|
||||
|
||||
@ -369,21 +372,15 @@ class InvenTreePart extends InvenTreeModel {
|
||||
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||
url,
|
||||
image,
|
||||
method: 'PATCH',
|
||||
name: 'image',
|
||||
method: "PATCH",
|
||||
name: "image",
|
||||
);
|
||||
|
||||
return response.successful();
|
||||
}
|
||||
|
||||
// Return the "starred" status of this part
|
||||
bool get starred => (jsondata['starred'] ?? false) as bool;
|
||||
|
||||
InvenTreePart() : super();
|
||||
|
||||
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
||||
// TODO
|
||||
}
|
||||
bool get starred => (jsondata["starred"] ?? false) as bool;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
@ -399,11 +396,11 @@ class InvenTreePartAttachment extends InvenTreeAttachment {
|
||||
|
||||
InvenTreePartAttachment() : super();
|
||||
|
||||
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "part/attachment/";
|
||||
|
||||
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
return InvenTreePartAttachment.fromJson(json);
|
||||
|
205
lib/inventree/purchase_order.dart
Normal file
205
lib/inventree/purchase_order.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import 'dart:io';
|
||||
import "dart:io";
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import "package:device_info_plus/device_info_plus.dart";
|
||||
import "package:package_info_plus/package_info_plus.dart";
|
||||
import "package:sentry_flutter/sentry_flutter.dart";
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
Future<Map<String, dynamic>> getDeviceInfo() async {
|
||||
|
||||
@ -18,35 +18,35 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
|
||||
final iosDeviceInfo = await deviceInfo.iosInfo;
|
||||
|
||||
device_info = {
|
||||
'name': iosDeviceInfo.name,
|
||||
'model': iosDeviceInfo.model,
|
||||
'systemName': iosDeviceInfo.systemName,
|
||||
'systemVersion': iosDeviceInfo.systemVersion,
|
||||
'localizedModel': iosDeviceInfo.localizedModel,
|
||||
'utsname': iosDeviceInfo.utsname.sysname,
|
||||
'identifierForVendor': iosDeviceInfo.identifierForVendor,
|
||||
'isPhysicalDevice': iosDeviceInfo.isPhysicalDevice,
|
||||
"name": iosDeviceInfo.name,
|
||||
"model": iosDeviceInfo.model,
|
||||
"systemName": iosDeviceInfo.systemName,
|
||||
"systemVersion": iosDeviceInfo.systemVersion,
|
||||
"localizedModel": iosDeviceInfo.localizedModel,
|
||||
"utsname": iosDeviceInfo.utsname.sysname,
|
||||
"identifierForVendor": iosDeviceInfo.identifierForVendor,
|
||||
"isPhysicalDevice": iosDeviceInfo.isPhysicalDevice,
|
||||
};
|
||||
|
||||
} else if (Platform.isAndroid) {
|
||||
final androidDeviceInfo = await deviceInfo.androidInfo;
|
||||
|
||||
device_info = {
|
||||
'type': androidDeviceInfo.type,
|
||||
'model': androidDeviceInfo.model,
|
||||
'device': androidDeviceInfo.device,
|
||||
'id': androidDeviceInfo.id,
|
||||
'androidId': androidDeviceInfo.androidId,
|
||||
'brand': androidDeviceInfo.brand,
|
||||
'display': androidDeviceInfo.display,
|
||||
'hardware': androidDeviceInfo.hardware,
|
||||
'manufacturer': androidDeviceInfo.manufacturer,
|
||||
'product': androidDeviceInfo.product,
|
||||
'version': androidDeviceInfo.version.release,
|
||||
'supported32BitAbis': androidDeviceInfo.supported32BitAbis,
|
||||
'supported64BitAbis': androidDeviceInfo.supported64BitAbis,
|
||||
'supportedAbis': androidDeviceInfo.supportedAbis,
|
||||
'isPhysicalDevice': androidDeviceInfo.isPhysicalDevice,
|
||||
"type": androidDeviceInfo.type,
|
||||
"model": androidDeviceInfo.model,
|
||||
"device": androidDeviceInfo.device,
|
||||
"id": androidDeviceInfo.id,
|
||||
"androidId": androidDeviceInfo.androidId,
|
||||
"brand": androidDeviceInfo.brand,
|
||||
"display": androidDeviceInfo.display,
|
||||
"hardware": androidDeviceInfo.hardware,
|
||||
"manufacturer": androidDeviceInfo.manufacturer,
|
||||
"product": androidDeviceInfo.product,
|
||||
"version": androidDeviceInfo.version.release,
|
||||
"supported32BitAbis": androidDeviceInfo.supported32BitAbis,
|
||||
"supported64BitAbis": androidDeviceInfo.supported64BitAbis,
|
||||
"supportedAbis": androidDeviceInfo.supportedAbis,
|
||||
"isPhysicalDevice": androidDeviceInfo.isPhysicalDevice,
|
||||
};
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
|
||||
|
||||
if (isInDebugMode()) {
|
||||
|
||||
print('----- In dev mode. Not sending message to Sentry.io -----');
|
||||
print("----- In dev mode. Not sending message to Sentry.io -----");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
|
||||
|
||||
Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
|
||||
|
||||
print('----- Sentry Intercepted error: $error -----');
|
||||
print("----- Sentry Intercepted error: $error -----");
|
||||
print(stackTrace);
|
||||
|
||||
// Errors thrown in development mode are unlikely to be interesting. You can
|
||||
@ -125,7 +125,7 @@ Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
|
||||
// the report.
|
||||
if (isInDebugMode()) {
|
||||
|
||||
print('----- In dev mode. Not sending report to Sentry.io -----');
|
||||
print("----- In dev mode. Not sending report to Sentry.io -----");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,23 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'model.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
|
||||
class InvenTreeStockItemTestResult extends InvenTreeModel {
|
||||
|
||||
InvenTreeStockItemTestResult() : super();
|
||||
|
||||
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "stock/test/";
|
||||
|
||||
@ -31,23 +35,17 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
|
||||
};
|
||||
}
|
||||
|
||||
String get key => jsondata['key'] ?? '';
|
||||
String get key => (jsondata["key"] ?? "") as String;
|
||||
|
||||
String get testName => jsondata['test'] ?? '';
|
||||
String get testName => (jsondata["test"] ?? "") as String;
|
||||
|
||||
bool get result => jsondata['result'] ?? false;
|
||||
bool get result => (jsondata["result"] ?? false) as bool;
|
||||
|
||||
String get value => jsondata['value'] ?? '';
|
||||
String get value => (jsondata["value"] ?? "") as String;
|
||||
|
||||
String get notes => jsondata['notes'] ?? '';
|
||||
String get attachment => (jsondata["attachment"] ?? "") as String;
|
||||
|
||||
String get attachment => jsondata['attachment'] ?? '';
|
||||
|
||||
String get date => jsondata['date'] ?? '';
|
||||
|
||||
InvenTreeStockItemTestResult() : super();
|
||||
|
||||
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
String get date => (jsondata["date"] ?? "") as String;
|
||||
|
||||
@override
|
||||
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
|
||||
@ -60,6 +58,10 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
|
||||
|
||||
class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
InvenTreeStockItem() : super();
|
||||
|
||||
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
// Stock status codes
|
||||
static const int OK = 10;
|
||||
static const int ATTENTION = 50;
|
||||
@ -97,7 +99,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
Color get statusColor {
|
||||
switch (status) {
|
||||
case OK:
|
||||
return Color(0xFF50aa51);
|
||||
return Colors.black;
|
||||
case ATTENTION:
|
||||
return Color(0xFFfdc82a);
|
||||
case DAMAGED:
|
||||
@ -114,7 +116,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
String get URL => "stock/";
|
||||
|
||||
@override
|
||||
String WEB_URL = "stock/item/";
|
||||
String get WEB_URL => "stock/item/";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
@ -132,33 +134,24 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
@override
|
||||
Map<String, String> defaultGetFilters() {
|
||||
|
||||
var headers = new Map<String, String>();
|
||||
|
||||
headers["part_detail"] = "true";
|
||||
headers["location_detail"] = "true";
|
||||
headers["supplier_detail"] = "true";
|
||||
headers["cascade"] = "false";
|
||||
|
||||
return headers;
|
||||
return {
|
||||
"part_detail": "true",
|
||||
"location_detail": "true",
|
||||
"supplier_detail": "true",
|
||||
"cascade": "false"
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
|
||||
var headers = new Map<String, String>();
|
||||
|
||||
headers["part_detail"] = "true";
|
||||
headers["location_detail"] = "true";
|
||||
headers["supplier_detail"] = "true";
|
||||
headers["cascade"] = "false";
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
InvenTreeStockItem() : super();
|
||||
|
||||
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
||||
// TODO
|
||||
return {
|
||||
"part_detail": "true",
|
||||
"location_detail": "true",
|
||||
"supplier_detail": "true",
|
||||
"cascade": "false",
|
||||
"in_stock": "true",
|
||||
};
|
||||
}
|
||||
|
||||
List<InvenTreePartTestTemplate> testTemplates = [];
|
||||
@ -204,17 +197,17 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
});
|
||||
}
|
||||
|
||||
String get uid => jsondata['uid'] ?? '';
|
||||
String get uid => (jsondata["uid"] ?? "") as String;
|
||||
|
||||
int get status => jsondata['status'] ?? -1;
|
||||
int get status => (jsondata["status"] ?? -1) as int;
|
||||
|
||||
String get packaging => jsondata["packaging"] ?? "";
|
||||
String get packaging => (jsondata["packaging"] ?? "") as String;
|
||||
|
||||
String get batch => jsondata["batch"] ?? "";
|
||||
String get batch => (jsondata["batch"] ?? "") as String;
|
||||
|
||||
int get partId => jsondata['part'] ?? -1;
|
||||
int get partId => (jsondata["part"] ?? -1) as int;
|
||||
|
||||
String get purchasePrice => jsondata['purchase_price'] ?? "";
|
||||
String get purchasePrice => (jsondata["purchase_price"] ?? "") as String;
|
||||
|
||||
bool get hasPurchasePrice {
|
||||
|
||||
@ -223,12 +216,14 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
return pp.isNotEmpty && pp.trim() != "-";
|
||||
}
|
||||
|
||||
int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
|
||||
int get purchaseOrderId => (jsondata["purchase_order"] ?? -1) as int;
|
||||
|
||||
int get trackingItemCount => (jsondata["tracking_items"] ?? 0) as int;
|
||||
|
||||
// Date of last update
|
||||
DateTime? get updatedDate {
|
||||
if (jsondata.containsKey("updated")) {
|
||||
return DateTime.tryParse(jsondata["updated"] ?? '');
|
||||
return DateTime.tryParse((jsondata["updated"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -248,7 +243,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
DateTime? get stocktakeDate {
|
||||
if (jsondata.containsKey("stocktake_date")) {
|
||||
return DateTime.tryParse(jsondata["stocktake_date"] ?? '');
|
||||
return DateTime.tryParse((jsondata["stocktake_date"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -268,45 +263,45 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
String get partName {
|
||||
|
||||
String nm = '';
|
||||
String nm = "";
|
||||
|
||||
// Use the detailed part information as priority
|
||||
if (jsondata.containsKey('part_detail')) {
|
||||
nm = jsondata['part_detail']['full_name'] ?? '';
|
||||
if (jsondata.containsKey("part_detail")) {
|
||||
nm = (jsondata["part_detail"]["full_name"] ?? "") as String;
|
||||
}
|
||||
|
||||
// Backup if first value fails
|
||||
if (nm.isEmpty) {
|
||||
nm = jsondata['part__name'] ?? '';
|
||||
nm = (jsondata["part__name"] ?? "") as String;
|
||||
}
|
||||
|
||||
return nm;
|
||||
}
|
||||
|
||||
String get partDescription {
|
||||
String desc = '';
|
||||
String desc = "";
|
||||
|
||||
// Use the detailed part description as priority
|
||||
if (jsondata.containsKey('part_detail')) {
|
||||
desc = jsondata['part_detail']['description'] ?? '';
|
||||
if (jsondata.containsKey("part_detail")) {
|
||||
desc = (jsondata["part_detail"]["description"] ?? "") as String;
|
||||
}
|
||||
|
||||
if (desc.isEmpty) {
|
||||
desc = jsondata['part__description'] ?? '';
|
||||
desc = (jsondata["part__description"] ?? "") as String;
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
String get partImage {
|
||||
String img = '';
|
||||
String img = "";
|
||||
|
||||
if (jsondata.containsKey('part_detail')) {
|
||||
img = jsondata['part_detail']['thumbnail'] ?? '';
|
||||
if (jsondata.containsKey("part_detail")) {
|
||||
img = (jsondata["part_detail"]["thumbnail"] ?? "") as String;
|
||||
}
|
||||
|
||||
if (img.isEmpty) {
|
||||
img = jsondata['part__thumbnail'] ?? '';
|
||||
img = (jsondata["part__thumbnail"] ?? "") as String;
|
||||
}
|
||||
|
||||
return img;
|
||||
@ -319,107 +314,97 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
String thumb = "";
|
||||
|
||||
thumb = jsondata['part_detail']?['thumbnail'] ?? '';
|
||||
thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
|
||||
|
||||
// Use 'image' as a backup
|
||||
// Use "image" as a backup
|
||||
if (thumb.isEmpty) {
|
||||
thumb = jsondata['part_detail']?['image'] ?? '';
|
||||
thumb = (jsondata["part_detail"]?["image"] ?? "") as String;
|
||||
}
|
||||
|
||||
// Try a different approach
|
||||
if (thumb.isEmpty) {
|
||||
thumb = jsondata['part__thumbnail'] ?? '';
|
||||
thumb = (jsondata["part__thumbnail"] ?? "") as String;
|
||||
}
|
||||
|
||||
// Still no thumbnail? Use the 'no image' image
|
||||
// Still no thumbnail? Use the "no image" image
|
||||
if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb;
|
||||
|
||||
return thumb;
|
||||
}
|
||||
|
||||
int get supplierPartId => (jsondata['supplier_part'] ?? -1) as int;
|
||||
int get supplierPartId => (jsondata["supplier_part"] ?? -1) as int;
|
||||
|
||||
String get supplierImage {
|
||||
String thumb = '';
|
||||
String thumb = "";
|
||||
|
||||
if (jsondata.containsKey("supplier_detail")) {
|
||||
thumb = jsondata['supplier_detail']['supplier_logo'] ?? '';
|
||||
thumb = (jsondata["supplier_detail"]["supplier_logo"] ?? "") as String;
|
||||
}
|
||||
|
||||
return thumb;
|
||||
}
|
||||
|
||||
String get supplierName {
|
||||
String sname = '';
|
||||
String sname = "";
|
||||
|
||||
if (jsondata.containsKey("supplier_detail")) {
|
||||
sname = jsondata["supplier_detail"]["supplier_name"] ?? '';
|
||||
sname = (jsondata["supplier_detail"]["supplier_name"] ?? "") as String;
|
||||
}
|
||||
|
||||
return sname;
|
||||
}
|
||||
|
||||
String get units {
|
||||
return jsondata['part_detail']?['units'] ?? '';
|
||||
return (jsondata["part_detail"]?["units"] ?? "") as String;
|
||||
}
|
||||
|
||||
String get supplierSKU {
|
||||
String sku = '';
|
||||
String sku = "";
|
||||
|
||||
if (jsondata.containsKey("supplier_detail")) {
|
||||
sku = jsondata["supplier_detail"]["SKU"] ?? '';
|
||||
sku = (jsondata["supplier_detail"]["SKU"] ?? "") as String;
|
||||
}
|
||||
|
||||
return sku;
|
||||
}
|
||||
|
||||
String get serialNumber => jsondata['serial'] ?? "";
|
||||
String get serialNumber => (jsondata["serial"] ?? "") as String;
|
||||
|
||||
double get quantity => double.tryParse(jsondata['quantity'].toString()) ?? 0;
|
||||
double get quantity => double.tryParse(jsondata["quantity"].toString()) ?? 0;
|
||||
|
||||
String get quantityString {
|
||||
String quantityString({bool includeUnits = false}){
|
||||
|
||||
String q = quantity.toString();
|
||||
String q = simpleNumberString(quantity);
|
||||
|
||||
// Simplify integer values e.g. "1.0" becomes "1"
|
||||
if (quantity.toInt() == quantity) {
|
||||
q = quantity.toInt().toString();
|
||||
}
|
||||
|
||||
if (units.isNotEmpty) {
|
||||
if (includeUnits && units.isNotEmpty) {
|
||||
q += " ${units}";
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
int get locationId => (jsondata['location'] ?? -1) as int;
|
||||
int get locationId => (jsondata["location"] ?? -1) as int;
|
||||
|
||||
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
|
||||
|
||||
String serialOrQuantityDisplay() {
|
||||
if (isSerialized()) {
|
||||
return 'SN ${serialNumber}';
|
||||
return "SN ${serialNumber}";
|
||||
}
|
||||
|
||||
// Is an integer?
|
||||
if (quantity.toInt() == quantity) {
|
||||
return '${quantity.toInt()}';
|
||||
}
|
||||
|
||||
return '${quantity}';
|
||||
return simpleNumberString(quantity);
|
||||
}
|
||||
|
||||
String get locationName {
|
||||
String loc = '';
|
||||
String loc = "";
|
||||
|
||||
if (locationId == -1 || !jsondata.containsKey('location_detail')) return 'Unknown Location';
|
||||
if (locationId == -1 || !jsondata.containsKey("location_detail")) return "Unknown Location";
|
||||
|
||||
loc = jsondata['location_detail']['name'] ?? '';
|
||||
loc = (jsondata["location_detail"]["name"] ?? "") as String;
|
||||
|
||||
// Old-style name
|
||||
if (loc.isEmpty) {
|
||||
loc = jsondata['location__name'] ?? '';
|
||||
loc = (jsondata["location__name"] ?? "") as String;
|
||||
}
|
||||
|
||||
return loc;
|
||||
@ -427,9 +412,9 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
String get locationPathString {
|
||||
|
||||
if (locationId == -1 || !jsondata.containsKey('location_detail')) return L10().locationNotSet;
|
||||
if (locationId == -1 || !jsondata.containsKey("location_detail")) return L10().locationNotSet;
|
||||
|
||||
String _loc = jsondata['location_detail']['pathstring'] ?? '';
|
||||
String _loc = (jsondata["location_detail"]["pathstring"] ?? "") as String;
|
||||
|
||||
if (_loc.isNotEmpty) {
|
||||
return _loc;
|
||||
@ -444,7 +429,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
if (serialNumber.isNotEmpty) {
|
||||
return "SN: $serialNumber";
|
||||
} else {
|
||||
return quantityString;
|
||||
return simpleNumberString(quantity);
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,7 +466,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
"pk": "${pk}",
|
||||
"quantity": "${q}",
|
||||
},
|
||||
"notes": notes ?? '',
|
||||
"notes": notes ?? "",
|
||||
},
|
||||
expectedStatusCode: 200
|
||||
);
|
||||
@ -489,6 +474,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
return response.isValid();
|
||||
}
|
||||
|
||||
// TODO: Refactor this once the server supports API metadata for this action
|
||||
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/count/", q, notes: notes);
|
||||
@ -496,6 +482,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Refactor this once the server supports API metadata for this action
|
||||
Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
|
||||
@ -503,6 +490,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Refactor this once the server supports API metadata for this action
|
||||
Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
|
||||
@ -510,6 +498,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Refactor this once the server supports API metadata for this action
|
||||
Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
|
||||
if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) {
|
||||
quantity = this.quantity;
|
||||
@ -535,10 +524,14 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
class InvenTreeStockLocation extends InvenTreeModel {
|
||||
|
||||
InvenTreeStockLocation() : super();
|
||||
|
||||
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "stock/location/";
|
||||
|
||||
String get pathstring => jsondata['pathstring'] ?? '';
|
||||
String get pathstring => (jsondata["pathstring"] ?? "") as String;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
@ -551,13 +544,13 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
||||
|
||||
String get parentpathstring {
|
||||
// TODO - Drive the refactor tractor through this
|
||||
List<String> psplit = pathstring.split('/');
|
||||
List<String> psplit = pathstring.split("/");
|
||||
|
||||
if (psplit.length > 0) {
|
||||
if (psplit.isNotEmpty) {
|
||||
psplit.removeLast();
|
||||
}
|
||||
|
||||
String p = psplit.join('/');
|
||||
String p = psplit.join("/");
|
||||
|
||||
if (p.isEmpty) {
|
||||
p = "Top level stock location";
|
||||
@ -566,11 +559,7 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
||||
return p;
|
||||
}
|
||||
|
||||
int get itemcount => jsondata['items'] ?? 0;
|
||||
|
||||
InvenTreeStockLocation() : super();
|
||||
|
||||
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
int get itemcount => (jsondata["items"] ?? 0) as int;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations_en.dart';
|
||||
import "package:flutter_gen/gen_l10n/app_localizations.dart";
|
||||
import "package:flutter_gen/gen_l10n/app_localizations_en.dart";
|
||||
|
||||
import 'package:one_context/one_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
// Shortcut function to reduce boilerplate!
|
||||
I18N L10()
|
||||
|
2
lib/l10n
2
lib/l10n
@ -1 +1 @@
|
||||
Subproject commit 3c7806d03887b8380efa22b8c1ca0e3eca2b98ad
|
||||
Subproject commit d004dc013edfc47b5ed94c5b019a013dc5ef444a
|
@ -1,19 +1,18 @@
|
||||
import 'dart:async';
|
||||
import "dart:async";
|
||||
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import "package:flutter_localizations/flutter_localizations.dart";
|
||||
import "package:flutter_gen/gen_l10n/app_localizations.dart";
|
||||
|
||||
import 'package:inventree/widget/home.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_context/one_context.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:package_info_plus/package_info_plus.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:sentry_flutter/sentry_flutter.dart";
|
||||
|
||||
import 'dsn.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/dsn.dart";
|
||||
import "package:inventree/widget/home.dart";
|
||||
|
||||
|
||||
Future<void> main() async {
|
||||
@ -75,24 +74,24 @@ class InvenTreeApp extends StatelessWidget {
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: [
|
||||
const Locale('de', ''), // German
|
||||
const Locale('el', ''), // Greek
|
||||
const Locale('en', ''), // English
|
||||
const Locale('es', ''), // Spanish
|
||||
const Locale('fr', ''), // French
|
||||
const Locale('he', ''), // Hebrew
|
||||
const Locale('it', ''), // Italian
|
||||
const Locale('ja', ''), // Japanese
|
||||
const Locale('ko', ''), // Korean
|
||||
const Locale('nl', ''), // Dutch
|
||||
const Locale('no', ''), // Norwegian
|
||||
const Locale('pl', ''), // Polish
|
||||
const Locale('ru', ''), // Russian
|
||||
const Locale('sv', ''), // Swedish
|
||||
const Locale('th', ''), // Thai
|
||||
const Locale('tr', ''), // Turkish
|
||||
const Locale('vi', ''), // Vietnamese
|
||||
const Locale('zh-CN', ''), // Chinese
|
||||
const Locale("de", ""), // German
|
||||
const Locale("el", ""), // Greek
|
||||
const Locale("en", ""), // English
|
||||
const Locale("es", ""), // Spanish
|
||||
const Locale("fr", ""), // French
|
||||
const Locale("he", ""), // Hebrew
|
||||
const Locale("it", ""), // Italian
|
||||
const Locale("ja", ""), // Japanese
|
||||
const Locale("ko", ""), // Korean
|
||||
const Locale("nl", ""), // Dutch
|
||||
const Locale("no", ""), // Norwegian
|
||||
const Locale("pl", ""), // Polish
|
||||
const Locale("ru", ""), // Russian
|
||||
const Locale("sv", ""), // Swedish
|
||||
const Locale("th", ""), // Thai
|
||||
const Locale("tr", ""), // Turkish
|
||||
const Locale("vi", ""), // Vietnamese
|
||||
const Locale("zh-CN", ""), // Chinese
|
||||
],
|
||||
|
||||
);
|
||||
|
@ -1,20 +1,22 @@
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sembast/sembast.dart';
|
||||
import 'package:sembast/sembast_io.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'dart:async';
|
||||
import "dart:async";
|
||||
|
||||
import "package:path_provider/path_provider.dart";
|
||||
import "package:sembast/sembast.dart";
|
||||
import "package:sembast/sembast_io.dart";
|
||||
import "package:path/path.dart";
|
||||
|
||||
|
||||
/*
|
||||
* Class for storing InvenTree preferences in a NoSql DB
|
||||
*/
|
||||
class InvenTreePreferencesDB {
|
||||
|
||||
InvenTreePreferencesDB._();
|
||||
|
||||
static final InvenTreePreferencesDB _singleton = InvenTreePreferencesDB._();
|
||||
|
||||
static InvenTreePreferencesDB get instance => _singleton;
|
||||
|
||||
InvenTreePreferencesDB._();
|
||||
|
||||
Completer<Database> _dbOpenCompleter = Completer();
|
||||
|
||||
bool isOpen = false;
|
||||
@ -34,7 +36,7 @@ class InvenTreePreferencesDB {
|
||||
return _dbOpenCompleter.future;
|
||||
}
|
||||
|
||||
Future _openDatabase() async {
|
||||
Future<void> _openDatabase() async {
|
||||
// Get a platform-specific directory where persistent app data can be stored
|
||||
final appDocumentDir = await getApplicationDocumentsDirectory();
|
||||
|
||||
@ -43,7 +45,7 @@ class InvenTreePreferencesDB {
|
||||
print("Path: ${appDocumentDir.path}");
|
||||
|
||||
// Path with the form: /platform-specific-directory/demo.db
|
||||
final dbPath = join(appDocumentDir.path, 'InvenTreeSettings.db');
|
||||
final dbPath = join(appDocumentDir.path, "InvenTreeSettings.db");
|
||||
|
||||
final database = await databaseFactoryIo.openDatabase(dbPath);
|
||||
|
||||
@ -54,8 +56,14 @@ class InvenTreePreferencesDB {
|
||||
|
||||
class InvenTreePreferences {
|
||||
|
||||
factory InvenTreePreferences() {
|
||||
return _api;
|
||||
}
|
||||
|
||||
InvenTreePreferences._internal();
|
||||
|
||||
/* The following settings are not stored to persistent storage,
|
||||
* instead they are only used as 'session preferences'.
|
||||
* instead they are only used as "session preferences".
|
||||
* They are kept here as a convenience only.
|
||||
*/
|
||||
|
||||
@ -72,11 +80,6 @@ class InvenTreePreferences {
|
||||
bool expandStockList = true;
|
||||
|
||||
// Ensure we only ever create a single instance of the preferences class
|
||||
static final InvenTreePreferences _api = new InvenTreePreferences._internal();
|
||||
static final InvenTreePreferences _api = InvenTreePreferences._internal();
|
||||
|
||||
factory InvenTreePreferences() {
|
||||
return _api;
|
||||
}
|
||||
|
||||
InvenTreePreferences._internal();
|
||||
}
|
@ -1,22 +1,22 @@
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/settings/release.dart';
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/settings/release.dart";
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:package_info_plus/package_info_plus.dart";
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
class InvenTreeAboutWidget extends StatelessWidget {
|
||||
|
||||
const InvenTreeAboutWidget(this.info) : super();
|
||||
|
||||
final PackageInfo info;
|
||||
|
||||
InvenTreeAboutWidget(this.info) : super();
|
||||
|
||||
void _releaseNotes(BuildContext context) async {
|
||||
Future <void> _releaseNotes(BuildContext context) async {
|
||||
|
||||
// Load release notes from external file
|
||||
String notes = await rootBundle.loadString("assets/release_notes.md");
|
||||
@ -27,7 +27,7 @@ class InvenTreeAboutWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _credits(BuildContext context) async {
|
||||
Future <void> _credits(BuildContext context) async {
|
||||
|
||||
String notes = await rootBundle.loadString("assets/credits.md");
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import 'package:inventree/app_settings.dart';
|
||||
import "package:inventree/app_settings.dart";
|
||||
|
||||
class InvenTreeAppSettingsWidget extends StatefulWidget {
|
||||
@override
|
||||
@ -15,10 +15,10 @@ class InvenTreeAppSettingsWidget extends StatefulWidget {
|
||||
|
||||
class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
|
||||
|
||||
final GlobalKey<_InvenTreeAppSettingsState> _settingsKey = GlobalKey<_InvenTreeAppSettingsState>();
|
||||
|
||||
_InvenTreeAppSettingsState();
|
||||
|
||||
final GlobalKey<_InvenTreeAppSettingsState> _settingsKey = GlobalKey<_InvenTreeAppSettingsState>();
|
||||
|
||||
bool barcodeSounds = true;
|
||||
bool serverSounds = true;
|
||||
bool partSubcategory = false;
|
||||
@ -31,7 +31,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
void loadSettings() async {
|
||||
Future <void> loadSettings() async {
|
||||
barcodeSounds = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
|
||||
serverSounds = await InvenTreeSettingsManager().getValue("serverSounds", true) as bool;
|
||||
|
||||
@ -42,35 +42,35 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void setBarcodeSounds(bool en) async {
|
||||
Future <void> setBarcodeSounds(bool en) async {
|
||||
|
||||
await InvenTreeSettingsManager().setValue("barcodeSounds", en);
|
||||
barcodeSounds = await InvenTreeSettingsManager().getValue("barcodeSounds", true);
|
||||
barcodeSounds = await InvenTreeSettingsManager().getBool("barcodeSounds", true);
|
||||
|
||||
setState(() {
|
||||
});
|
||||
}
|
||||
|
||||
void setServerSounds(bool en) async {
|
||||
Future <void> setServerSounds(bool en) async {
|
||||
|
||||
await InvenTreeSettingsManager().setValue("serverSounds", en);
|
||||
serverSounds = await InvenTreeSettingsManager().getValue("serverSounds", true);
|
||||
serverSounds = await InvenTreeSettingsManager().getBool("serverSounds", true);
|
||||
|
||||
setState(() {
|
||||
});
|
||||
}
|
||||
|
||||
void setPartSubcategory(bool en) async {
|
||||
Future <void> setPartSubcategory(bool en) async {
|
||||
await InvenTreeSettingsManager().setValue("partSubcategory", en);
|
||||
partSubcategory = await InvenTreeSettingsManager().getValue("partSubcategory", true);
|
||||
partSubcategory = await InvenTreeSettingsManager().getBool("partSubcategory", true);
|
||||
|
||||
setState(() {
|
||||
});
|
||||
}
|
||||
|
||||
void setStockSublocation(bool en) async {
|
||||
Future <void> setStockSublocation(bool en) async {
|
||||
await InvenTreeSettingsManager().setValue("stockSublocation", en);
|
||||
stockSublocation = await InvenTreeSettingsManager().getValue("stockSublocation", true);
|
||||
stockSublocation = await InvenTreeSettingsManager().getBool("stockSublocation", true);
|
||||
|
||||
setState(() {
|
||||
});
|
||||
|
@ -1,15 +1,12 @@
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/widget/spinner.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import '../api.dart';
|
||||
import '../user_profile.dart';
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/spinner.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
|
||||
class InvenTreeLoginSettingsWidget extends StatefulWidget {
|
||||
|
||||
@ -20,17 +17,15 @@ class InvenTreeLoginSettingsWidget extends StatefulWidget {
|
||||
|
||||
class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
|
||||
final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>();
|
||||
|
||||
final GlobalKey<FormState> _addProfileKey = new GlobalKey<FormState>();
|
||||
|
||||
List<UserProfile> profiles = [];
|
||||
|
||||
_InvenTreeLoginSettingsState() {
|
||||
_reload();
|
||||
}
|
||||
|
||||
void _reload() async {
|
||||
final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>();
|
||||
|
||||
List<UserProfile> profiles = [];
|
||||
|
||||
Future <void> _reload() async {
|
||||
|
||||
profiles = await UserProfileDBManager().getAllProfiles();
|
||||
|
||||
@ -40,17 +35,6 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
|
||||
void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) {
|
||||
|
||||
var _name;
|
||||
var _server;
|
||||
var _username;
|
||||
var _password;
|
||||
|
||||
UserProfile? profile;
|
||||
|
||||
if (userProfile != null) {
|
||||
profile = userProfile;
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@ -61,7 +45,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void _selectProfile(BuildContext context, UserProfile profile) async {
|
||||
Future <void> _selectProfile(BuildContext context, UserProfile profile) async {
|
||||
|
||||
// Disconnect InvenTree
|
||||
InvenTreeAPI().disconnectFromServer();
|
||||
@ -84,42 +68,24 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
_reload();
|
||||
}
|
||||
|
||||
void _deleteProfile(UserProfile profile) async {
|
||||
Future <void> _deleteProfile(UserProfile profile) async {
|
||||
|
||||
await UserProfileDBManager().deleteProfile(profile);
|
||||
|
||||
_reload();
|
||||
|
||||
if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
|
||||
if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? "")) {
|
||||
InvenTreeAPI().disconnectFromServer();
|
||||
}
|
||||
}
|
||||
|
||||
void _updateProfile(UserProfile? profile) async {
|
||||
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_reload();
|
||||
|
||||
if (InvenTreeAPI().isConnected() && InvenTreeAPI().profile != null && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
|
||||
// Attempt server login (this will load the newly selected profile
|
||||
|
||||
InvenTreeAPI().connectToServer().then((result) {
|
||||
_reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Widget? _getProfileIcon(UserProfile profile) {
|
||||
|
||||
// Not selected? No icon for you!
|
||||
if (!profile.selected) return null;
|
||||
|
||||
// Selected, but (for some reason) not the same as the API...
|
||||
if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
|
||||
if ((InvenTreeAPI().profile?.key ?? "") != profile.key) {
|
||||
return FaIcon(
|
||||
FontAwesomeIcons.questionCircle,
|
||||
color: COLOR_WARNING
|
||||
@ -150,7 +116,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
|
||||
List<Widget> children = [];
|
||||
|
||||
if (profiles.length > 0) {
|
||||
if (profiles.isNotEmpty) {
|
||||
for (int idx = 0; idx < profiles.length; idx++) {
|
||||
UserProfile profile = profiles[idx];
|
||||
|
||||
@ -253,9 +219,9 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
|
||||
class ProfileEditWidget extends StatefulWidget {
|
||||
|
||||
UserProfile? profile;
|
||||
const ProfileEditWidget(this.profile) : super();
|
||||
|
||||
ProfileEditWidget(this.profile) : super();
|
||||
final UserProfile? profile;
|
||||
|
||||
@override
|
||||
_ProfileEditState createState() => _ProfileEditState(profile);
|
||||
@ -263,11 +229,11 @@ class ProfileEditWidget extends StatefulWidget {
|
||||
|
||||
class _ProfileEditState extends State<ProfileEditWidget> {
|
||||
|
||||
UserProfile? profile;
|
||||
|
||||
_ProfileEditState(this.profile) : super();
|
||||
|
||||
final formKey = new GlobalKey<FormState>();
|
||||
UserProfile? profile;
|
||||
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
String name = "";
|
||||
String server = "";
|
||||
@ -375,7 +341,7 @@ class _ProfileEditState extends State<ProfileEditWidget> {
|
||||
|
||||
if (uri.hasScheme) {
|
||||
print("Scheme: ${uri.scheme}");
|
||||
if (!(["http", "https"].contains(uri.scheme.toLowerCase()))) {
|
||||
if (!["http", "https"].contains(uri.scheme.toLowerCase())) {
|
||||
return L10().serverStart;
|
||||
}
|
||||
} else {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_markdown/flutter_markdown.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
|
||||
class ReleaseNotesWidget extends StatelessWidget {
|
||||
|
||||
final String releaseNotes;
|
||||
const ReleaseNotesWidget(this.releaseNotes);
|
||||
|
||||
ReleaseNotesWidget(this.releaseNotes);
|
||||
final String releaseNotes;
|
||||
|
||||
@override
|
||||
Widget build (BuildContext context) {
|
||||
@ -27,9 +27,9 @@ class ReleaseNotesWidget extends StatelessWidget {
|
||||
|
||||
class CreditsWidget extends StatelessWidget {
|
||||
|
||||
final String credits;
|
||||
const CreditsWidget(this.credits);
|
||||
|
||||
CreditsWidget(this.credits);
|
||||
final String credits;
|
||||
|
||||
@override
|
||||
Widget build (BuildContext context) {
|
||||
|
@ -1,18 +1,16 @@
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/settings/about.dart';
|
||||
import 'package:inventree/settings/app_settings.dart';
|
||||
import 'package:inventree/settings/login.dart';
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/settings/about.dart";
|
||||
import "package:inventree/settings/app_settings.dart";
|
||||
import "package:inventree/settings/login.dart";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import 'package:inventree/widget/submit_feedback.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/widget/submit_feedback.dart";
|
||||
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
import 'login.dart';
|
||||
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import "package:package_info_plus/package_info_plus.dart";
|
||||
|
||||
class InvenTreeSettingsWidget extends StatefulWidget {
|
||||
// InvenTree settings view
|
||||
@ -95,30 +93,30 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
||||
}
|
||||
|
||||
|
||||
void _openDocs() async {
|
||||
Future <void> _openDocs() async {
|
||||
if (await canLaunch(docsUrl)) {
|
||||
await launch(docsUrl);
|
||||
}
|
||||
}
|
||||
|
||||
void _translate() async {
|
||||
final String url = "https://crowdin.com/project/inventree";
|
||||
Future <void> _translate() async {
|
||||
const String url = "https://crowdin.com/project/inventree";
|
||||
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
}
|
||||
}
|
||||
|
||||
void _editServerSettings() async {
|
||||
Future <void> _editServerSettings() async {
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget()));
|
||||
}
|
||||
|
||||
void _editAppSettings() async {
|
||||
Future <void> _editAppSettings() async {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeAppSettingsWidget()));
|
||||
}
|
||||
|
||||
void _about() async {
|
||||
Future <void> _about() async {
|
||||
|
||||
PackageInfo.fromPlatform().then((PackageInfo info) {
|
||||
Navigator.push(context,
|
||||
@ -126,7 +124,7 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
void _submitFeedback(BuildContext context) async {
|
||||
Future <void> _submitFeedback(BuildContext context) async {
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
|
@ -2,8 +2,8 @@
|
||||
/*
|
||||
* Class for InvenTree user / login details
|
||||
*/
|
||||
import 'package:sembast/sembast.dart';
|
||||
import 'preferences.dart';
|
||||
import "package:sembast/sembast.dart";
|
||||
import "preferences.dart";
|
||||
|
||||
class UserProfile {
|
||||
|
||||
@ -16,6 +16,15 @@ class UserProfile {
|
||||
this.selected = false,
|
||||
});
|
||||
|
||||
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
|
||||
key: key,
|
||||
name: json["name"] as String,
|
||||
server: json["server"] as String,
|
||||
username: json["username"] as String,
|
||||
password: json["password"] as String,
|
||||
selected: isSelected,
|
||||
);
|
||||
|
||||
// ID of the profile
|
||||
int? key;
|
||||
|
||||
@ -36,15 +45,6 @@ class UserProfile {
|
||||
// User ID (will be provided by the server on log-in)
|
||||
int user_id = -1;
|
||||
|
||||
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
|
||||
key: key,
|
||||
name: json['name'],
|
||||
server: json['server'],
|
||||
username: json['username'],
|
||||
password: json['password'],
|
||||
selected: isSelected,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"name": name,
|
||||
"server": server,
|
||||
@ -62,7 +62,7 @@ class UserProfileDBManager {
|
||||
|
||||
final store = StoreRef("profiles");
|
||||
|
||||
Future<Database> get _db async => await InvenTreePreferencesDB.instance.database;
|
||||
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
|
||||
|
||||
Future<bool> profileNameExists(String name) async {
|
||||
|
||||
@ -70,10 +70,10 @@ class UserProfileDBManager {
|
||||
|
||||
final profiles = await store.find(await _db, finder: finder);
|
||||
|
||||
return profiles.length > 0;
|
||||
return profiles.isNotEmpty;
|
||||
}
|
||||
|
||||
Future addProfile(UserProfile profile) async {
|
||||
Future<void> addProfile(UserProfile profile) async {
|
||||
|
||||
// Check if a profile already exists with the name
|
||||
final bool exists = await profileNameExists(profile.name);
|
||||
@ -83,7 +83,7 @@ class UserProfileDBManager {
|
||||
return;
|
||||
}
|
||||
|
||||
int key = await store.add(await _db, profile.toJson());
|
||||
int key = await store.add(await _db, profile.toJson()) as int;
|
||||
|
||||
print("Added user profile <${key}> - '${profile.name}'");
|
||||
|
||||
@ -91,7 +91,7 @@ class UserProfileDBManager {
|
||||
profile.key = key;
|
||||
}
|
||||
|
||||
Future selectProfile(int key) async {
|
||||
Future<void> selectProfile(int key) async {
|
||||
/*
|
||||
* Mark the particular profile as selected
|
||||
*/
|
||||
@ -101,7 +101,7 @@ class UserProfileDBManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future updateProfile(UserProfile profile) async {
|
||||
Future<void> updateProfile(UserProfile profile) async {
|
||||
|
||||
if (profile.key == null) {
|
||||
await addProfile(profile);
|
||||
@ -115,7 +115,7 @@ class UserProfileDBManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future deleteProfile(UserProfile profile) async {
|
||||
Future<void> deleteProfile(UserProfile profile) async {
|
||||
await store.record(profile.key).delete(await _db);
|
||||
print("Deleted user profile <${profile.key}> - '${profile.name}'");
|
||||
}
|
||||
@ -135,8 +135,8 @@ class UserProfileDBManager {
|
||||
|
||||
if (profiles[idx].key is int && profiles[idx].key == selected) {
|
||||
return UserProfile.fromJson(
|
||||
profiles[idx].key,
|
||||
profiles[idx].value,
|
||||
profiles[idx].key as int,
|
||||
profiles[idx].value as Map<String, dynamic>,
|
||||
profiles[idx].key == selected,
|
||||
);
|
||||
}
|
||||
@ -161,8 +161,8 @@ class UserProfileDBManager {
|
||||
if (profiles[idx].key is int) {
|
||||
profileList.add(
|
||||
UserProfile.fromJson(
|
||||
profiles[idx].key,
|
||||
profiles[idx].value,
|
||||
profiles[idx].key as int,
|
||||
profiles[idx].value as Map<String, dynamic>,
|
||||
profiles[idx].key == selected,
|
||||
));
|
||||
}
|
||||
|
27
lib/widget/back.dart
Normal file
27
lib/widget/back.dart
Normal 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();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
@ -1,27 +1,22 @@
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/app_settings.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/part_list.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/widget/part_detail.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
|
||||
import 'package:inventree/widget/part_detail.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/paginator.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
class CategoryDisplayWidget extends StatefulWidget {
|
||||
|
||||
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
||||
const CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreePartCategory? category;
|
||||
|
||||
@ -32,6 +27,7 @@ class CategoryDisplayWidget extends StatefulWidget {
|
||||
|
||||
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
|
||||
_CategoryDisplayState(this.category);
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().partCategory;
|
||||
@ -41,7 +37,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
|
||||
List<Widget> actions = [];
|
||||
|
||||
if ((category != null) && InvenTreeAPI().checkPermission('part_category', 'change')) {
|
||||
if ((category != null) && InvenTreeAPI().checkPermission("part_category", "change")) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
@ -74,8 +70,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
_CategoryDisplayState(this.category);
|
||||
|
||||
// The local InvenTreePartCategory object
|
||||
final InvenTreePartCategory? category;
|
||||
|
||||
@ -199,7 +193,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
|
||||
if (loading) {
|
||||
tiles.add(progressIndicator());
|
||||
} else if (_subcategories.length == 0) {
|
||||
} else if (_subcategories.isEmpty) {
|
||||
tiles.add(ListTile(
|
||||
title: Text(L10().noSubcategories),
|
||||
subtitle: Text(
|
||||
@ -224,8 +218,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
data: {
|
||||
"parent": (pk > 0) ? pk : null,
|
||||
},
|
||||
onSuccess: (data) async {
|
||||
|
||||
onSuccess: (result) async {
|
||||
|
||||
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||
|
||||
if (data.containsKey("pk")) {
|
||||
var cat = InvenTreePartCategory.fromJson(data);
|
||||
|
||||
@ -252,7 +248,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
data: {
|
||||
"category": (pk > 0) ? pk : null
|
||||
},
|
||||
onSuccess: (data) async {
|
||||
onSuccess: (result) async {
|
||||
|
||||
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||
|
||||
if (data.containsKey("pk")) {
|
||||
var part = InvenTreePart.fromJson(data);
|
||||
@ -274,7 +272,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
getCategoryDescriptionCard(extra: false),
|
||||
];
|
||||
|
||||
if (InvenTreeAPI().checkPermission('part', 'add')) {
|
||||
if (InvenTreeAPI().checkPermission("part", "add")) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().categoryCreate),
|
||||
@ -298,7 +296,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
if (tiles.length == 0) {
|
||||
if (tiles.isEmpty) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(
|
||||
@ -327,7 +325,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
);
|
||||
case 1:
|
||||
return PaginatedPartList(
|
||||
{"category": "${category?.pk ?? null}"},
|
||||
{
|
||||
"category": "${category?.pk ?? 'null'}"
|
||||
},
|
||||
);
|
||||
case 2:
|
||||
return ListView(
|
||||
@ -344,9 +344,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
* Builder for displaying a list of PartCategory objects
|
||||
*/
|
||||
class SubcategoryList extends StatelessWidget {
|
||||
final List<InvenTreePartCategory> _categories;
|
||||
|
||||
SubcategoryList(this._categories);
|
||||
const SubcategoryList(this._categories);
|
||||
|
||||
final List<InvenTreePartCategory> _categories;
|
||||
|
||||
void _openCategory(BuildContext context, int pk) {
|
||||
|
||||
@ -381,170 +382,3 @@ class SubcategoryList extends StatelessWidget {
|
||||
itemBuilder: _build, itemCount: _categories.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Widget for displaying a list of Part objects within a PartCategory display.
|
||||
*
|
||||
* Uses server-side pagination for snappy results
|
||||
*/
|
||||
|
||||
class PaginatedPartList extends StatefulWidget {
|
||||
|
||||
final Map<String, String> filters;
|
||||
|
||||
Function(int)? onTotalChanged;
|
||||
|
||||
PaginatedPartList(this.filters, {this.onTotalChanged});
|
||||
|
||||
@override
|
||||
_PaginatedPartListState createState() => _PaginatedPartListState(filters, onTotalChanged);
|
||||
}
|
||||
|
||||
|
||||
class _PaginatedPartListState extends State<PaginatedPartList> {
|
||||
|
||||
static const _pageSize = 25;
|
||||
|
||||
String _searchTerm = "";
|
||||
|
||||
Function(int)? onTotalChanged;
|
||||
|
||||
final Map<String, String> filters;
|
||||
|
||||
_PaginatedPartListState(this.filters, this.onTotalChanged);
|
||||
|
||||
final PagingController<int, InvenTreePart> _pagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int resultCount = 0;
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
try {
|
||||
|
||||
Map<String, String> params = filters;
|
||||
|
||||
params["search"] = _searchTerm;
|
||||
|
||||
final bool cascade = await InvenTreeSettingsManager().getValue("partSubcategory", true);
|
||||
params["cascade"] = "${cascade}";
|
||||
|
||||
final page = await InvenTreePart().listPaginated(_pageSize, pageKey, filters: params);
|
||||
int pageLength = page?.length ?? 0;
|
||||
int pageCount = page?.count ?? 0;
|
||||
|
||||
final isLastPage = pageLength < _pageSize;
|
||||
|
||||
// Construct a list of part objects
|
||||
List<InvenTreePart> parts = [];
|
||||
|
||||
if (page != null) {
|
||||
for (var result in page.results) {
|
||||
if (result is InvenTreePart) {
|
||||
parts.add(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLastPage) {
|
||||
_pagingController.appendLastPage(parts);
|
||||
} else {
|
||||
final int nextPageKey = pageKey + pageLength;
|
||||
_pagingController.appendPage(parts, nextPageKey);
|
||||
}
|
||||
|
||||
if (onTotalChanged != null) {
|
||||
onTotalChanged!(pageCount);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
resultCount = pageCount;
|
||||
});
|
||||
|
||||
} catch (error, stackTrace) {
|
||||
print("Error! - ${error.toString()}");
|
||||
_pagingController.error = error;
|
||||
|
||||
sentryReportError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _openPart(BuildContext context, int pk) {
|
||||
// Attempt to load the part information
|
||||
InvenTreePart().get(pk).then((var part) {
|
||||
if (part is InvenTreePart) {
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPart(BuildContext context, InvenTreePart part) {
|
||||
return ListTile(
|
||||
title: Text(part.fullname),
|
||||
subtitle: Text("${part.description}"),
|
||||
trailing: Text("${part.inStockString}"),
|
||||
leading: InvenTreeAPI().getImage(
|
||||
part.thumbnail,
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
onTap: () {
|
||||
_openPart(context, part.pk);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
void updateSearchTerm() {
|
||||
|
||||
_searchTerm = searchController.text;
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
physics: ClampingScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
slivers: [
|
||||
PagedSliverList.separated(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<InvenTreePart>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return _buildPart(context, item);
|
||||
},
|
||||
noItemsFoundIndicatorBuilder: (context) {
|
||||
return NoResultsWidget(L10().partNoResults);
|
||||
},
|
||||
),
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
81
lib/widget/category_list.dart
Normal file
81
lib/widget/category_list.dart
Normal 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)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,19 +1,21 @@
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/api_form.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/inventree/company.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:inventree/inventree/purchase_order.dart";
|
||||
import "package:inventree/widget/purchase_order_list.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
|
||||
class CompanyDetailWidget extends StatefulWidget {
|
||||
|
||||
final InvenTreeCompany company;
|
||||
const CompanyDetailWidget(this.company, {Key? key}) : super(key: key);
|
||||
|
||||
CompanyDetailWidget(this.company, {Key? key}) : super(key: key);
|
||||
final InvenTreeCompany company;
|
||||
|
||||
@override
|
||||
_CompanyDetailState createState() => _CompanyDetailState(company);
|
||||
@ -27,6 +29,8 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
||||
|
||||
final InvenTreeCompany company;
|
||||
|
||||
List<InvenTreePurchaseOrder> outstandingOrders = [];
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().company;
|
||||
|
||||
@ -61,9 +65,13 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
||||
@override
|
||||
Future<void> request() async {
|
||||
await company.reload();
|
||||
|
||||
if (company.isSupplier) {
|
||||
outstandingOrders = await company.getPurchaseOrders(outstanding: true);
|
||||
}
|
||||
}
|
||||
|
||||
void editCompany(BuildContext context) async {
|
||||
Future <void> editCompany(BuildContext context) async {
|
||||
|
||||
company.editForm(
|
||||
context,
|
||||
@ -146,6 +154,37 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
||||
// TODO - Add list of purchase orders
|
||||
|
||||
tiles.add(Divider());
|
||||
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().purchaseOrders),
|
||||
leading: FaIcon(FontAwesomeIcons.shoppingCart, color: COLOR_CLICK),
|
||||
trailing: Text("${outstandingOrders.length}"),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PurchaseOrderListWidget(
|
||||
filters: {
|
||||
"supplier": "${company.pk}"
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: Display "supplied parts" count (click through to list of supplier parts)
|
||||
/*
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().suppliedParts),
|
||||
leading: FaIcon(FontAwesomeIcons.shapes),
|
||||
trailing: Text("${company.partSuppliedCount}"),
|
||||
)
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
if (company.isManufacturer) {
|
||||
|
@ -1,25 +1,22 @@
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/inventree/company.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/widget/paginator.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/company_detail.dart';
|
||||
|
||||
import '../l10.dart';
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/widget/paginator.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/company_detail.dart";
|
||||
|
||||
|
||||
class CompanyListWidget extends StatefulWidget {
|
||||
|
||||
CompanyListWidget(this.title, this.filters, {Key? key}) : super(key: key);
|
||||
const CompanyListWidget(this.title, this.filters, {Key? key}) : super(key: key);
|
||||
|
||||
String title;
|
||||
final String title;
|
||||
|
||||
Map<String, String> filters;
|
||||
final Map<String, String> filters;
|
||||
|
||||
@override
|
||||
_CompanyListWidgetState createState() => _CompanyListWidgetState(title, filters);
|
||||
@ -49,103 +46,32 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
|
||||
|
||||
class PaginatedCompanyList extends StatefulWidget {
|
||||
|
||||
PaginatedCompanyList(this.filters, {this.onTotalChanged});
|
||||
const PaginatedCompanyList(this.filters, {this.onTotalChanged});
|
||||
|
||||
final Map<String, String> filters;
|
||||
|
||||
Function(int)? onTotalChanged;
|
||||
final Function(int)? onTotalChanged;
|
||||
|
||||
@override
|
||||
_CompanyListState createState() => _CompanyListState(filters, onTotalChanged);
|
||||
_CompanyListState createState() => _CompanyListState(filters);
|
||||
}
|
||||
|
||||
class _CompanyListState extends State<PaginatedCompanyList> {
|
||||
class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> {
|
||||
|
||||
_CompanyListState(this.filters, this.onTotalChanged);
|
||||
|
||||
static const _pageSize = 25;
|
||||
_CompanyListState(Map<String, String> filters) : super(filters);
|
||||
|
||||
String _searchTerm = "";
|
||||
|
||||
Function(int)? onTotalChanged;
|
||||
|
||||
final Map<String, String> filters;
|
||||
|
||||
final PagingController<int, InvenTreeCompany> _pagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
||||
|
||||
final page = await InvenTreeCompany().listPaginated(limit, offset, filters: params);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int resultCount = 0;
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
try {
|
||||
Map<String, String> params = filters;
|
||||
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
||||
|
||||
params["search"] = _searchTerm;
|
||||
|
||||
final page = await InvenTreeCompany().listPaginated(
|
||||
_pageSize, pageKey, filters: params);
|
||||
|
||||
int pageLength = page?.length ?? 0;
|
||||
int pageCount = page?.count ?? 0;
|
||||
|
||||
final isLastPage = pageLength < _pageSize;
|
||||
|
||||
List<InvenTreeCompany> companies = [];
|
||||
|
||||
if (page != null) {
|
||||
for (var result in page.results) {
|
||||
if (result is InvenTreeCompany) {
|
||||
companies.add(result);
|
||||
} else {
|
||||
print(result.jsondata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLastPage) {
|
||||
_pagingController.appendLastPage(companies);
|
||||
} else {
|
||||
final int nextPageKey = pageKey + pageLength;
|
||||
_pagingController.appendPage(companies, nextPageKey);
|
||||
}
|
||||
|
||||
if (onTotalChanged != null) {
|
||||
onTotalChanged!(pageCount);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
resultCount = pageCount;
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
print("Error! - ${error.toString()}");
|
||||
_pagingController.error = error;
|
||||
|
||||
sentryReportError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void updateSearchTerm() {
|
||||
_searchTerm = searchController.text;
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
Widget _buildCompany(BuildContext context, InvenTreeCompany company) {
|
||||
InvenTreeCompany company = model as InvenTreeCompany;
|
||||
|
||||
return ListTile(
|
||||
title: Text(company.name),
|
||||
@ -160,36 +86,4 @@ class _CompanyListState extends State<PaginatedCompanyList> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
physics: ClampingScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
slivers: [
|
||||
PagedSliverList.separated(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<InvenTreeCompany>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return _buildCompany(context, item);
|
||||
},
|
||||
noItemsFoundIndicatorBuilder: (context) {
|
||||
return NoResultsWidget(L10().companyNoResults);
|
||||
}
|
||||
),
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
|
||||
import 'package:inventree/app_settings.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import 'package:one_context/one_context.dart';
|
||||
import "package:inventree/app_settings.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:audioplayers/audioplayers.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
Future<void> confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {
|
||||
|
||||
|
@ -1,21 +1,17 @@
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/barcode.dart';
|
||||
import 'package:inventree/widget/company_list.dart';
|
||||
import 'package:inventree/widget/search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/barcode.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import 'package:inventree/widget/category_display.dart';
|
||||
import 'package:inventree/widget/location_display.dart';
|
||||
|
||||
import 'package:inventree/settings/settings.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import "package:inventree/settings/settings.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/widget/search.dart";
|
||||
|
||||
class InvenTreeDrawer extends StatelessWidget {
|
||||
|
||||
final BuildContext context;
|
||||
const InvenTreeDrawer(this.context);
|
||||
|
||||
InvenTreeDrawer(this.context);
|
||||
final BuildContext context;
|
||||
|
||||
void _closeDrawer() {
|
||||
// Close the drawer
|
||||
@ -29,7 +25,9 @@ class InvenTreeDrawer extends StatelessWidget {
|
||||
void _home() {
|
||||
_closeDrawer();
|
||||
|
||||
Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false);
|
||||
while (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _search() {
|
||||
@ -38,65 +36,25 @@ class InvenTreeDrawer extends StatelessWidget {
|
||||
|
||||
_closeDrawer();
|
||||
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: PartSearchDelegate(context)
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SearchWidget()
|
||||
)
|
||||
);
|
||||
|
||||
//Navigator.push(context, MaterialPageRoute(builder: (context) => SearchWidget()));
|
||||
}
|
||||
|
||||
/*
|
||||
* Launch the camera to scan a QR code.
|
||||
* Upon successful scan, data are passed off to be decoded.
|
||||
*/
|
||||
void _scan() async {
|
||||
Future <void> _scan() async {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
_closeDrawer();
|
||||
scanQrCode(context);
|
||||
}
|
||||
|
||||
/*
|
||||
* Display the top-level PartCategory list
|
||||
*/
|
||||
void _showParts() {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
_closeDrawer();
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
||||
}
|
||||
|
||||
/*
|
||||
* Display the top-level StockLocation list
|
||||
*/
|
||||
void _showStock() {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
_closeDrawer();
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
|
||||
}
|
||||
|
||||
void _showSuppliers() {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
_closeDrawer();
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
|
||||
}
|
||||
|
||||
void _showManufacturers() {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
_closeDrawer();
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
|
||||
}
|
||||
|
||||
void _showCustomers() {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
_closeDrawer();
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
|
||||
}
|
||||
|
||||
/*
|
||||
* Load settings widget
|
||||
*/
|
||||
@ -107,17 +65,14 @@ class InvenTreeDrawer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: <Widget>[
|
||||
ListTile(
|
||||
leading: Image.asset(
|
||||
"assets/image/icon.png",
|
||||
fit: BoxFit.scaleDown,
|
||||
width: 30,
|
||||
),
|
||||
leading: FaIcon(FontAwesomeIcons.home),
|
||||
title: Text(
|
||||
L10().appTitle,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
@ -134,35 +89,6 @@ class InvenTreeDrawer extends StatelessWidget {
|
||||
leading: FaIcon(FontAwesomeIcons.search),
|
||||
onTap: _search,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10().parts),
|
||||
leading: Icon(Icons.category),
|
||||
onTap: _showParts,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10().stock),
|
||||
leading: FaIcon(FontAwesomeIcons.boxes),
|
||||
onTap: _showStock,
|
||||
),
|
||||
|
||||
/*
|
||||
ListTile(
|
||||
title: Text("Suppliers"),
|
||||
leading: FaIcon(FontAwesomeIcons.building),
|
||||
onTap: _showSuppliers,
|
||||
),
|
||||
ListTile(
|
||||
title: Text("Manufacturers"),
|
||||
leading: FaIcon(FontAwesomeIcons.industry),
|
||||
onTap: _showManufacturers,
|
||||
),
|
||||
ListTile(
|
||||
title: Text("Customers"),
|
||||
leading: FaIcon(FontAwesomeIcons.users),
|
||||
onTap: _showCustomers,
|
||||
),
|
||||
*/
|
||||
|
||||
ListTile(
|
||||
title: Text(L10().settings),
|
||||
leading: Icon(Icons.settings),
|
||||
|
@ -1,15 +1,13 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:one_context/one_context.dart';
|
||||
import "package:file_picker/file_picker.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:image_picker/image_picker.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
|
||||
class FilePickerDialog {
|
||||
@ -167,7 +165,7 @@ class CheckBoxField extends FormField<bool> {
|
||||
|
||||
class StringField extends TextFormField {
|
||||
|
||||
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) :
|
||||
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function(String?)? validator, bool allowEmpty = false, bool isEnabled = true}) :
|
||||
super(
|
||||
decoration: InputDecoration(
|
||||
labelText: allowEmpty ? label : label + "*",
|
||||
@ -182,7 +180,7 @@ class StringField extends TextFormField {
|
||||
}
|
||||
|
||||
if (validator != null) {
|
||||
return validator(value);
|
||||
return validator(value) as String?;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -196,7 +194,7 @@ class StringField extends TextFormField {
|
||||
*/
|
||||
class QuantityField extends TextFormField {
|
||||
|
||||
QuantityField({String label = "", String hint = "", String initial = "", double? max, TextEditingController? controller}) :
|
||||
QuantityField({String label = "", String hint = "", double? max, TextEditingController? controller}) :
|
||||
super(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
|
@ -1,27 +1,28 @@
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/user_profile.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/settings/settings.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/barcode.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/settings/login.dart";
|
||||
import "package:inventree/widget/category_display.dart";
|
||||
import "package:inventree/widget/company_list.dart";
|
||||
import "package:inventree/widget/location_display.dart";
|
||||
import "package:inventree/widget/part_list.dart";
|
||||
import "package:inventree/widget/purchase_order_list.dart";
|
||||
import "package:inventree/widget/search.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:inventree/widget/drawer.dart";
|
||||
|
||||
import 'package:inventree/barcode.dart';
|
||||
import 'package:inventree/api.dart';
|
||||
|
||||
import 'package:inventree/settings/login.dart';
|
||||
|
||||
import 'package:inventree/widget/category_display.dart';
|
||||
import 'package:inventree/widget/company_list.dart';
|
||||
import 'package:inventree/widget/location_display.dart';
|
||||
import 'package:inventree/widget/search.dart';
|
||||
import 'package:inventree/widget/spinner.dart';
|
||||
import 'package:inventree/widget/drawer.dart';
|
||||
|
||||
class InvenTreeHomePage extends StatefulWidget {
|
||||
|
||||
InvenTreeHomePage({Key? key}) : super(key: key);
|
||||
const InvenTreeHomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_InvenTreeHomePageState createState() => _InvenTreeHomePageState();
|
||||
@ -29,33 +30,27 @@ class InvenTreeHomePage extends StatefulWidget {
|
||||
|
||||
class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
|
||||
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
|
||||
|
||||
_InvenTreeHomePageState() : super() {
|
||||
|
||||
// Initially load the profile and attempt server connection
|
||||
_loadProfile();
|
||||
}
|
||||
|
||||
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
|
||||
|
||||
// Selected user profile
|
||||
UserProfile? _profile;
|
||||
|
||||
void _searchParts() {
|
||||
void _search(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: PartSearchDelegate(context)
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SearchWidget()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _searchStock() {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: StockSearchDelegate(context)
|
||||
);
|
||||
}
|
||||
|
||||
void _scan(BuildContext context) {
|
||||
@ -64,31 +59,60 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
scanQrCode(context);
|
||||
}
|
||||
|
||||
void _parts(BuildContext context) {
|
||||
void _showParts(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
||||
}
|
||||
|
||||
void _stock(BuildContext context) {
|
||||
void _showSettings(BuildContext context) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget()));
|
||||
}
|
||||
|
||||
void _showStarredParts(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PartList({
|
||||
"starred": "true"
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _showStock(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
|
||||
}
|
||||
|
||||
void _suppliers() {
|
||||
void _showPurchaseOrders(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PurchaseOrderListWidget(filters: {})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _showSuppliers(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
|
||||
}
|
||||
|
||||
void _manufacturers() {
|
||||
void _showManufacturers(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
|
||||
}
|
||||
|
||||
void _customers() {
|
||||
void _showCustomers(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
|
||||
@ -103,7 +127,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
});
|
||||
}
|
||||
|
||||
void _loadProfile() async {
|
||||
Future <void> _loadProfile() async {
|
||||
|
||||
_profile = await UserProfileDBManager().getSelectedProfile();
|
||||
|
||||
@ -121,269 +145,180 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
ListTile _serverTile() {
|
||||
|
||||
// No profile selected
|
||||
// Tap to select / create a profile
|
||||
if (_profile == null) {
|
||||
return ListTile(
|
||||
title: Text(L10().profileNotSelected),
|
||||
subtitle: Text(L10().profileTapToCreate),
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
trailing: FaIcon(
|
||||
FontAwesomeIcons.user,
|
||||
color: COLOR_DANGER,
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile();
|
||||
},
|
||||
);
|
||||
Widget _iconButton(BuildContext context, String label, IconData icon, {Function()? callback, String role = "", String permission = ""}) {
|
||||
|
||||
bool connected = InvenTreeAPI().isConnected();
|
||||
|
||||
bool allowed = true;
|
||||
|
||||
if (role.isNotEmpty || permission.isNotEmpty) {
|
||||
allowed = InvenTreeAPI().checkPermission(role, permission);
|
||||
}
|
||||
|
||||
// Profile is selected ...
|
||||
if (InvenTreeAPI().isConnecting()) {
|
||||
return ListTile(
|
||||
title: Text(L10().serverConnecting),
|
||||
subtitle: Text("${InvenTreeAPI().baseUrl}"),
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
trailing: Spinner(
|
||||
icon: FontAwesomeIcons.spinner,
|
||||
color: COLOR_PROGRESS,
|
||||
return GestureDetector(
|
||||
child: Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 10
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile();
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FaIcon(
|
||||
icon,
|
||||
color: connected && allowed ? COLOR_CLICK : Colors.grey,
|
||||
),
|
||||
Divider(
|
||||
height: 12,
|
||||
thickness: 0,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
onTap: () {
|
||||
|
||||
if (!allowed) {
|
||||
showSnackIcon(
|
||||
L10().permissionRequired,
|
||||
icon: FontAwesomeIcons.exclamationCircle,
|
||||
success: false,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
);
|
||||
} else if (InvenTreeAPI().isConnected()) {
|
||||
return ListTile(
|
||||
title: Text(L10().serverConnected),
|
||||
subtitle: Text("${InvenTreeAPI().baseUrl}"),
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
trailing: FaIcon(
|
||||
FontAwesomeIcons.checkCircle,
|
||||
color: COLOR_SUCCESS
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return ListTile(
|
||||
title: Text(L10().serverCouldNotConnect),
|
||||
subtitle: Text("${_profile!.server}"),
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
trailing: FaIcon(
|
||||
FontAwesomeIcons.timesCircle,
|
||||
color: COLOR_DANGER,
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> getGridTiles(BuildContext context) {
|
||||
return [
|
||||
_iconButton(
|
||||
context,
|
||||
L10().scanBarcode,
|
||||
FontAwesomeIcons.barcode,
|
||||
callback: () {
|
||||
_scan(context);
|
||||
}
|
||||
),
|
||||
_iconButton(
|
||||
context,
|
||||
L10().search,
|
||||
FontAwesomeIcons.search,
|
||||
callback: () {
|
||||
_search(context);
|
||||
}
|
||||
),
|
||||
_iconButton(
|
||||
context,
|
||||
L10().parts,
|
||||
FontAwesomeIcons.shapes,
|
||||
callback: () {
|
||||
_showParts(context);
|
||||
}
|
||||
),
|
||||
_iconButton(
|
||||
context,
|
||||
L10().partsStarred,
|
||||
FontAwesomeIcons.solidStar,
|
||||
callback: () {
|
||||
_showStarredParts(context);
|
||||
}
|
||||
),
|
||||
_iconButton(
|
||||
context,
|
||||
L10().stock,
|
||||
FontAwesomeIcons.boxes,
|
||||
callback: () {
|
||||
_showStock(context);
|
||||
}
|
||||
),
|
||||
_iconButton(
|
||||
context,
|
||||
L10().purchaseOrders,
|
||||
FontAwesomeIcons.shoppingCart,
|
||||
callback: () {
|
||||
_showPurchaseOrders(context);
|
||||
}
|
||||
),
|
||||
/*
|
||||
_iconButton(
|
||||
context,
|
||||
L10().salesOrders,
|
||||
FontAwesomeIcons.truck,
|
||||
),
|
||||
*/
|
||||
_iconButton(
|
||||
context,
|
||||
L10().suppliers,
|
||||
FontAwesomeIcons.building,
|
||||
callback: () {
|
||||
_showSuppliers(context);
|
||||
}
|
||||
),
|
||||
_iconButton(
|
||||
context,
|
||||
L10().manufacturers,
|
||||
FontAwesomeIcons.industry,
|
||||
callback: () {
|
||||
_showManufacturers(context);
|
||||
}
|
||||
),
|
||||
_iconButton(
|
||||
context,
|
||||
L10().customers,
|
||||
FontAwesomeIcons.userTie,
|
||||
callback: () {
|
||||
_showCustomers(context);
|
||||
}
|
||||
),
|
||||
_iconButton(
|
||||
context,
|
||||
L10().settings,
|
||||
FontAwesomeIcons.cogs,
|
||||
callback: () {
|
||||
_showSettings(context);
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
key: _homeKey,
|
||||
appBar: AppBar(
|
||||
title: Text(L10().appTitle),
|
||||
actions: <Widget>[
|
||||
/*
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.search),
|
||||
tooltip: L10().search,
|
||||
onPressed: _searchParts,
|
||||
),
|
||||
*/
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.server,
|
||||
color: InvenTreeAPI().isConnected() ? COLOR_SUCCESS : COLOR_DANGER,
|
||||
),
|
||||
onPressed: _selectProfile,
|
||||
)
|
||||
],
|
||||
),
|
||||
drawer: new InvenTreeDrawer(context),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: (<Widget>[
|
||||
Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.barcode),
|
||||
tooltip: L10().scanBarcode,
|
||||
onPressed: () { _scan(context); },
|
||||
),
|
||||
Text(L10().scanBarcode),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.shapes),
|
||||
tooltip: L10().parts,
|
||||
onPressed: () { _parts(context); },
|
||||
),
|
||||
Text(L10().parts),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.search),
|
||||
tooltip: L10().searchParts,
|
||||
onPressed: _searchParts,
|
||||
),
|
||||
Text(L10().searchParts),
|
||||
],
|
||||
),
|
||||
// TODO - Re-add starred parts link
|
||||
/*
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.solidStar),
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => StarredPartWidget()));
|
||||
},
|
||||
),
|
||||
Text("Starred Parts"),
|
||||
]
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.boxes),
|
||||
tooltip: L10().stock,
|
||||
onPressed: () { _stock(context); },
|
||||
),
|
||||
Text(L10().stock),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.search),
|
||||
tooltip: L10().searchStock,
|
||||
onPressed: _searchStock,
|
||||
),
|
||||
Text(L10().searchStock),
|
||||
],
|
||||
),
|
||||
]
|
||||
),
|
||||
Spacer(),
|
||||
// TODO - Re-add these when the features actually do something..
|
||||
/*
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.building),
|
||||
tooltip: "Suppliers",
|
||||
onPressed: _suppliers,
|
||||
),
|
||||
Text("Suppliers"),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.industry),
|
||||
tooltip: "Manufacturers",
|
||||
onPressed: _manufacturers,
|
||||
),
|
||||
Text("Manufacturers")
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.userTie),
|
||||
tooltip: "Customers",
|
||||
onPressed: _customers,
|
||||
),
|
||||
Text("Customers"),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.tools),
|
||||
tooltip: "Build",
|
||||
onPressed: _unsupported,
|
||||
),
|
||||
Text("Build"),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.shoppingCart),
|
||||
tooltip: "Order",
|
||||
onPressed: _unsupported,
|
||||
),
|
||||
Text("Order"),
|
||||
]
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: new FaIcon(FontAwesomeIcons.truck),
|
||||
tooltip: "Ship",
|
||||
onPressed: _unsupported,
|
||||
),
|
||||
Text("Ship"),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
Spacer(),
|
||||
*/
|
||||
Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _serverTile(),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
drawer: InvenTreeDrawer(context),
|
||||
body: ListView(
|
||||
children: [
|
||||
GridView.extent(
|
||||
maxCrossAxisExtent: 140,
|
||||
shrinkWrap: true,
|
||||
physics: ClampingScrollPhysics(),
|
||||
children: getGridTiles(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,21 +1,19 @@
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/app_settings.dart';
|
||||
import 'package:inventree/barcode.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/stock_detail.dart';
|
||||
import 'package:inventree/widget/paginator.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/barcode.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/stock_detail.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/widget/stock_list.dart";
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
class LocationDisplayWidget extends StatefulWidget {
|
||||
|
||||
@ -31,6 +29,8 @@ class LocationDisplayWidget extends StatefulWidget {
|
||||
|
||||
class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
|
||||
_LocationDisplayState(this.location);
|
||||
|
||||
final InvenTreeStockLocation? location;
|
||||
|
||||
@override
|
||||
@ -62,7 +62,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
);
|
||||
*/
|
||||
|
||||
if ((location != null) && (InvenTreeAPI().checkPermission('stock_location', 'change'))) {
|
||||
if ((location != null) && (InvenTreeAPI().checkPermission("stock_location", "change"))) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
@ -92,11 +92,9 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
_LocationDisplayState(this.location);
|
||||
|
||||
List<InvenTreeStockLocation> _sublocations = [];
|
||||
|
||||
String _locationFilter = '';
|
||||
String _locationFilter = "";
|
||||
|
||||
List<InvenTreeStockLocation> get sublocations {
|
||||
|
||||
@ -146,7 +144,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
data: {
|
||||
"parent": (pk > 0) ? pk : null,
|
||||
},
|
||||
onSuccess: (data) async {
|
||||
onSuccess: (result) async {
|
||||
|
||||
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||
|
||||
if (data.containsKey("pk")) {
|
||||
var loc = InvenTreeStockLocation.fromJson(data);
|
||||
|
||||
@ -175,7 +176,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
data: {
|
||||
"location": pk,
|
||||
},
|
||||
onSuccess: (data) async {
|
||||
onSuccess: (result) async {
|
||||
|
||||
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||
|
||||
if (data.containsKey("pk")) {
|
||||
var item = InvenTreeStockItem.fromJson(data);
|
||||
|
||||
@ -280,7 +284,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
children: detailTiles(),
|
||||
);
|
||||
case 1:
|
||||
return PaginatedStockList(filters);
|
||||
return PaginatedStockItemList(filters);
|
||||
case 2:
|
||||
return ListView(
|
||||
children: ListTile.divideTiles(
|
||||
@ -307,13 +311,13 @@ List<Widget> detailTiles() {
|
||||
L10().sublocations,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
trailing: sublocations.length > 0 ? Text("${sublocations.length}") : null,
|
||||
trailing: sublocations.isNotEmpty ? Text("${sublocations.length}") : null,
|
||||
),
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
tiles.add(progressIndicator());
|
||||
} else if (_sublocations.length > 0) {
|
||||
} else if (_sublocations.isNotEmpty) {
|
||||
tiles.add(SublocationList(_sublocations));
|
||||
} else {
|
||||
tiles.add(ListTile(
|
||||
@ -334,7 +338,7 @@ List<Widget> detailTiles() {
|
||||
|
||||
tiles.add(locationDescriptionCard(includeActions: false));
|
||||
|
||||
if (InvenTreeAPI().checkPermission('stock', 'add')) {
|
||||
if (InvenTreeAPI().checkPermission("stock", "add")) {
|
||||
|
||||
tiles.add(
|
||||
ListTile(
|
||||
@ -362,7 +366,7 @@ List<Widget> detailTiles() {
|
||||
|
||||
if (location != null) {
|
||||
// Stock adjustment actions
|
||||
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
||||
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
||||
// Scan items into location
|
||||
tiles.add(
|
||||
ListTile(
|
||||
@ -422,9 +426,10 @@ List<Widget> detailTiles() {
|
||||
|
||||
|
||||
class SublocationList extends StatelessWidget {
|
||||
final List<InvenTreeStockLocation> _locations;
|
||||
|
||||
SublocationList(this._locations);
|
||||
const SublocationList(this._locations);
|
||||
|
||||
final List<InvenTreeStockLocation> _locations;
|
||||
|
||||
void _openLocation(BuildContext context, int pk) {
|
||||
|
||||
@ -440,7 +445,7 @@ class SublocationList extends StatelessWidget {
|
||||
InvenTreeStockLocation loc = _locations[index];
|
||||
|
||||
return ListTile(
|
||||
title: Text('${loc.name}'),
|
||||
title: Text("${loc.name}"),
|
||||
subtitle: Text("${loc.description}"),
|
||||
trailing: Text("${loc.itemcount}"),
|
||||
onTap: () {
|
||||
@ -460,162 +465,3 @@ class SublocationList extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Widget for displaying a list of stock items within a stock location.
|
||||
*
|
||||
* Users server-side pagination for snappy results
|
||||
*/
|
||||
|
||||
class PaginatedStockList extends StatefulWidget {
|
||||
|
||||
final Map<String, String> filters;
|
||||
|
||||
PaginatedStockList(this.filters);
|
||||
|
||||
@override
|
||||
_PaginatedStockListState createState() => _PaginatedStockListState(filters);
|
||||
}
|
||||
|
||||
|
||||
class _PaginatedStockListState extends State<PaginatedStockList> {
|
||||
|
||||
static const _pageSize = 25;
|
||||
|
||||
String _searchTerm = "";
|
||||
|
||||
final Map<String, String> filters;
|
||||
|
||||
_PaginatedStockListState(this.filters);
|
||||
|
||||
final PagingController<int, InvenTreeStockItem> _pagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int resultCount = 0;
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
try {
|
||||
|
||||
Map<String, String> params = this.filters;
|
||||
|
||||
params["search"] = "${_searchTerm}";
|
||||
|
||||
// Do we include stock items from sub-locations?
|
||||
final bool cascade = await InvenTreeSettingsManager().getValue("stockSublocation", true);
|
||||
params["cascade"] = "${cascade}";
|
||||
|
||||
final page = await InvenTreeStockItem().listPaginated(_pageSize, pageKey, filters: params);
|
||||
|
||||
int pageLength = page?.length ?? 0;
|
||||
int pageCount = page?.count ?? 0;
|
||||
|
||||
final isLastPage = pageLength < _pageSize;
|
||||
|
||||
// Construct a list of stock item objects
|
||||
List<InvenTreeStockItem> items = [];
|
||||
|
||||
if (page != null) {
|
||||
for (var result in page.results) {
|
||||
if (result is InvenTreeStockItem) {
|
||||
items.add(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLastPage) {
|
||||
_pagingController.appendLastPage(items);
|
||||
} else {
|
||||
final int nextPageKey = pageKey + pageLength;
|
||||
_pagingController.appendPage(items, nextPageKey);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
resultCount = pageCount;
|
||||
});
|
||||
|
||||
} catch (error, stackTrace) {
|
||||
_pagingController.error = error;
|
||||
|
||||
sentryReportError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _openItem(BuildContext context, int pk) {
|
||||
InvenTreeStockItem().get(pk).then((var item) {
|
||||
if (item is InvenTreeStockItem) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, InvenTreeStockItem item) {
|
||||
return ListTile(
|
||||
title: Text("${item.partName}"),
|
||||
subtitle: Text("${item.locationPathString}"),
|
||||
leading: InvenTreeAPI().getImage(
|
||||
item.partThumbnail,
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
trailing: Text("${item.displayQuantity}",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
onTap: () {
|
||||
_openItem(context, item.pk);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
void updateSearchTerm() {
|
||||
_searchTerm = searchController.text;
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
physics: ClampingScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
slivers: <Widget>[
|
||||
// TODO - Search input
|
||||
PagedSliverList.separated(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<InvenTreeStockItem>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return _buildItem(context, item);
|
||||
},
|
||||
noItemsFoundIndicatorBuilder: (context) {
|
||||
return NoResultsWidget("No stock items found");
|
||||
}
|
||||
),
|
||||
separatorBuilder: (context, item) => const Divider(height: 1),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
82
lib/widget/location_list.dart
Normal file
82
lib/widget/location_list.dart
Normal 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)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,19 +1,158 @@
|
||||
// Pagination related widgets
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:infinite_scroll_pagination/infinite_scroll_pagination.dart";
|
||||
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
|
||||
class PaginatedSearchState<T extends StatefulWidget> extends State<T> {
|
||||
|
||||
PaginatedSearchState(this.filters);
|
||||
|
||||
final Map<String, String> filters;
|
||||
|
||||
static const _pageSize = 25;
|
||||
|
||||
// Search query term
|
||||
String searchTerm = "";
|
||||
|
||||
int resultCount = 0;
|
||||
|
||||
// Text controller
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
// Pagination controller
|
||||
final PagingController<int, InvenTreeModel> _pagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
||||
|
||||
print("Blank request page");
|
||||
// Default implementation returns null - must be overridden
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
try {
|
||||
Map<String, String> params = filters;
|
||||
|
||||
params["search"] = "${searchTerm}";
|
||||
|
||||
final page = await requestPage(
|
||||
_pageSize,
|
||||
pageKey,
|
||||
params
|
||||
);
|
||||
|
||||
int pageLength = page?.length ?? 0;
|
||||
int pageCount = page?.count ?? 0;
|
||||
|
||||
final isLastPage = pageLength < _pageSize;
|
||||
|
||||
List<InvenTreeModel> items = [];
|
||||
|
||||
if (page != null) {
|
||||
for (var result in page.results) {
|
||||
if (result is InvenTreeModel) {
|
||||
items.add(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLastPage) {
|
||||
_pagingController.appendLastPage(items);
|
||||
} else {
|
||||
final int nextPageKey = pageKey + pageLength;
|
||||
_pagingController.appendPage(items, nextPageKey);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
resultCount = pageCount;
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
_pagingController.error = error;
|
||||
|
||||
sentryReportError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void updateSearchTerm() {
|
||||
searchTerm = searchController.text;
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
Widget buildItem(BuildContext context, InvenTreeModel item) {
|
||||
|
||||
// This method must be overridden by the child class
|
||||
return ListTile(
|
||||
title: Text("*** UNIMPLEMENTED ***"),
|
||||
subtitle: Text("*** buildItem() is unimplemented for this widget!"),
|
||||
);
|
||||
}
|
||||
|
||||
String get noResultsText => L10().noResults;
|
||||
|
||||
@override
|
||||
Widget build (BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
physics: ClampingScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
slivers: <Widget>[
|
||||
// TODO - Search input
|
||||
PagedSliverList.separated(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return buildItem(context, item);
|
||||
},
|
||||
noItemsFoundIndicatorBuilder: (context) {
|
||||
return NoResultsWidget(noResultsText);
|
||||
}
|
||||
),
|
||||
separatorBuilder: (context, item) => const Divider(height: 1),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class PaginatedSearchWidget extends StatelessWidget {
|
||||
|
||||
Function onChanged;
|
||||
const PaginatedSearchWidget(this.controller, this.onChanged, this.results);
|
||||
|
||||
int results = 0;
|
||||
final Function onChanged;
|
||||
|
||||
TextEditingController controller;
|
||||
final int results;
|
||||
|
||||
PaginatedSearchWidget(this.controller, this.onChanged, this.results);
|
||||
final TextEditingController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -44,9 +183,9 @@ class PaginatedSearchWidget extends StatelessWidget {
|
||||
|
||||
class NoResultsWidget extends StatelessWidget {
|
||||
|
||||
final String description;
|
||||
const NoResultsWidget(this.description);
|
||||
|
||||
NoResultsWidget(this.description);
|
||||
final String description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -1,23 +1,19 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/fields.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import '../api.dart';
|
||||
import '../l10.dart';
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
class PartAttachmentsWidget extends StatefulWidget {
|
||||
|
||||
PartAttachmentsWidget(this.part, {Key? key}) : super(key: key);
|
||||
const PartAttachmentsWidget(this.part, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreePart part;
|
||||
|
||||
@ -42,7 +38,7 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
|
||||
|
||||
List<Widget> actions = [];
|
||||
|
||||
if (InvenTreeAPI().checkPermission('part', 'change')) {
|
||||
if (InvenTreeAPI().checkPermission("part", "change")) {
|
||||
|
||||
// File upload
|
||||
actions.add(
|
||||
@ -127,7 +123,7 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
|
||||
));
|
||||
}
|
||||
|
||||
if (tiles.length == 0) {
|
||||
if (tiles.isEmpty) {
|
||||
tiles.add(ListTile(
|
||||
title: Text(L10().attachmentNone),
|
||||
subtitle: Text(
|
||||
|
@ -1,28 +1,28 @@
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import 'package:inventree/widget/part_attachments_widget.dart';
|
||||
import 'package:inventree/widget/part_notes.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/category_display.dart';
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/part_image_widget.dart';
|
||||
import 'package:inventree/widget/stock_detail.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import 'location_display.dart';
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/widget/part_attachments_widget.dart";
|
||||
import "package:inventree/widget/part_notes.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/category_display.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/part_image_widget.dart";
|
||||
import "package:inventree/widget/stock_detail.dart";
|
||||
import "package:inventree/widget/stock_list.dart";
|
||||
|
||||
|
||||
class PartDetailWidget extends StatefulWidget {
|
||||
|
||||
PartDetailWidget(this.part, {Key? key}) : super(key: key);
|
||||
const PartDetailWidget(this.part, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreePart part;
|
||||
|
||||
@ -34,10 +34,10 @@ class PartDetailWidget extends StatefulWidget {
|
||||
|
||||
class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
|
||||
InvenTreePart part;
|
||||
|
||||
_PartDisplayState(this.part);
|
||||
|
||||
InvenTreePart part;
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().partDetails;
|
||||
|
||||
@ -46,7 +46,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
|
||||
List<Widget> actions = [];
|
||||
|
||||
if (InvenTreeAPI().checkPermission('part', 'view')) {
|
||||
if (InvenTreeAPI().checkPermission("part", "view")) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.globe),
|
||||
@ -55,7 +55,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
if (InvenTreeAPI().checkPermission('part', 'change')) {
|
||||
if (InvenTreeAPI().checkPermission("part", "change")) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
@ -89,9 +89,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
await part.getTestTemplates();
|
||||
}
|
||||
|
||||
void _toggleStar() async {
|
||||
Future <void> _toggleStar() async {
|
||||
|
||||
if (InvenTreeAPI().checkPermission('part', 'view')) {
|
||||
if (InvenTreeAPI().checkPermission("part", "view")) {
|
||||
await part.update(values: {"starred": "${!part.starred}"});
|
||||
refresh();
|
||||
}
|
||||
@ -327,7 +327,8 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
}
|
||||
|
||||
// TODO - Add request tests?
|
||||
if (false && part.isTrackable) {
|
||||
/*
|
||||
if (part.isTrackable) {
|
||||
tiles.add(ListTile(
|
||||
title: Text(L10().testsRequired),
|
||||
leading: FaIcon(FontAwesomeIcons.tasks),
|
||||
@ -336,6 +337,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
)
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
// Notes field
|
||||
tiles.add(
|
||||
@ -398,6 +400,12 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
|
||||
fields["part"]["hidden"] = true;
|
||||
|
||||
int? default_location = part.defaultLocation;
|
||||
|
||||
if (default_location != null) {
|
||||
fields["location"]["value"] = default_location;
|
||||
}
|
||||
|
||||
InvenTreeStockItem().createForm(
|
||||
context,
|
||||
L10().stockItemCreate,
|
||||
@ -405,7 +413,10 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
data: {
|
||||
"part": "${part.pk}",
|
||||
},
|
||||
onSuccess: (data) async {
|
||||
onSuccess: (result) async {
|
||||
|
||||
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||
|
||||
if (data.containsKey("pk")) {
|
||||
var item = InvenTreeStockItem.fromJson(data);
|
||||
|
||||
@ -437,20 +448,22 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
);
|
||||
|
||||
// TODO - Add this action back in once implemented
|
||||
if (false) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().barcodeScanItem),
|
||||
leading: FaIcon(FontAwesomeIcons.box),
|
||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||
onTap: () {
|
||||
// TODO
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (false && !part.isActive && InvenTreeAPI().checkPermission('part', 'delete')) {
|
||||
/*
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().barcodeScanItem),
|
||||
leading: FaIcon(FontAwesomeIcons.box),
|
||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||
onTap: () {
|
||||
// TODO
|
||||
},
|
||||
),
|
||||
);
|
||||
*/
|
||||
|
||||
/*
|
||||
// TODO: Implement part deletion
|
||||
if (!part.isActive && InvenTreeAPI().checkPermission("part", "delete")) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().deletePart),
|
||||
@ -462,6 +475,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
)
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
return tiles;
|
||||
}
|
||||
@ -480,7 +494,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
),
|
||||
);
|
||||
case 1:
|
||||
return PaginatedStockList({"part": "${part.pk}"});
|
||||
return PaginatedStockItemList(
|
||||
{"part": "${part.pk}"}
|
||||
);
|
||||
case 2:
|
||||
return Center(
|
||||
child: ListView(
|
||||
|
@ -1,23 +1,21 @@
|
||||
import 'dart:io';
|
||||
import "dart:io";
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
|
||||
import '../l10.dart';
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/fields.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
class PartImageWidget extends StatefulWidget {
|
||||
|
||||
PartImageWidget(this.part, {Key? key}) : super(key: key);
|
||||
const PartImageWidget(this.part, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreePart part;
|
||||
|
||||
@ -46,7 +44,7 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
|
||||
|
||||
List<Widget> actions = [];
|
||||
|
||||
if (InvenTreeAPI().checkPermission('part', 'change')) {
|
||||
if (InvenTreeAPI().checkPermission("part", "change")) {
|
||||
|
||||
// File upload
|
||||
actions.add(
|
||||
|
100
lib/widget/part_list.dart
Normal file
100
lib/widget/part_list.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter_markdown/flutter_markdown.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
|
||||
class PartNotesWidget extends StatefulWidget {
|
||||
|
||||
final InvenTreePart part;
|
||||
const PartNotesWidget(this.part, {Key? key}) : super(key: key);
|
||||
|
||||
PartNotesWidget(this.part, {Key? key}) : super(key: key);
|
||||
final InvenTreePart part;
|
||||
|
||||
@override
|
||||
_PartNotesState createState() => _PartNotesState(part);
|
||||
@ -21,10 +21,10 @@ class PartNotesWidget extends StatefulWidget {
|
||||
|
||||
class _PartNotesState extends RefreshableState<PartNotesWidget> {
|
||||
|
||||
final InvenTreePart part;
|
||||
|
||||
_PartNotesState(this.part);
|
||||
|
||||
final InvenTreePart part;
|
||||
|
||||
@override
|
||||
Future<void> request() async {
|
||||
await part.reload();
|
||||
@ -38,7 +38,7 @@ class _PartNotesState extends RefreshableState<PartNotesWidget> {
|
||||
|
||||
List<Widget> actions = [];
|
||||
|
||||
if (InvenTreeAPI().checkPermission('part', 'change')) {
|
||||
if (InvenTreeAPI().checkPermission("part", "change")) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
|
@ -1,19 +1,19 @@
|
||||
import 'package:inventree/l10.dart';
|
||||
import "dart:core";
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import 'dart:core';
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/company.dart';
|
||||
import 'package:inventree/widget/company_detail.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:inventree/widget/company_detail.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
|
||||
class PartSupplierWidget extends StatefulWidget {
|
||||
|
||||
PartSupplierWidget(this.part, {Key? key}) : super(key: key);
|
||||
const PartSupplierWidget(this.part, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreePart part;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
/*
|
||||
* Construct a circular progress indicator
|
||||
|
384
lib/widget/purchase_order_detail.dart
Normal file
384
lib/widget/purchase_order_detail.dart
Normal 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
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
96
lib/widget/purchase_order_list.dart
Normal file
96
lib/widget/purchase_order_list.dart
Normal 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)
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import 'package:inventree/widget/drawer.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import "package:inventree/widget/back.dart";
|
||||
import "package:inventree/widget/drawer.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
|
||||
abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
@ -9,7 +10,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
final refreshableKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
// Storage for context once "Build" is called
|
||||
BuildContext? _context;
|
||||
late BuildContext? _context;
|
||||
|
||||
// Current tab index (used for widgets which display bottom tabs)
|
||||
int tabIndex = 0;
|
||||
@ -32,6 +33,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
|
||||
String getAppBarTitle(BuildContext context) { return "App Bar Title"; }
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance?.addPostFrameCallback((_) => onBuild(_context!));
|
||||
@ -60,14 +62,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
});
|
||||
}
|
||||
|
||||
// Function to construct an appbar (override if needed)
|
||||
AppBar getAppBar(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Text(getAppBarTitle(context)),
|
||||
actions: getAppBarActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
// Function to construct a drawer (override if needed)
|
||||
Widget getDrawer(BuildContext context) {
|
||||
return InvenTreeDrawer(context);
|
||||
@ -96,8 +90,12 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
|
||||
return Scaffold(
|
||||
key: refreshableKey,
|
||||
appBar: getAppBar(context),
|
||||
drawer: null,
|
||||
appBar: AppBar(
|
||||
title: Text(getAppBarTitle(context)),
|
||||
actions: getAppBarActions(context),
|
||||
leading: backButton(context, refreshableKey),
|
||||
),
|
||||
drawer: getDrawer(context),
|
||||
floatingActionButton: getFab(context),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
|
@ -1,393 +1,347 @@
|
||||
import "dart:async";
|
||||
|
||||
import 'package:inventree/widget/part_detail.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import 'package:inventree/widget/stock_detail.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import '../api.dart';
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:inventree/inventree/purchase_order.dart";
|
||||
import "package:inventree/widget/part_list.dart";
|
||||
import "package:inventree/widget/purchase_order_list.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/widget/stock_list.dart";
|
||||
import "package:inventree/widget/category_list.dart";
|
||||
import "package:inventree/widget/company_list.dart";
|
||||
import "package:inventree/widget/location_list.dart";
|
||||
|
||||
// TODO - Refactor duplicate code in this file!
|
||||
|
||||
class PartSearchDelegate extends SearchDelegate<InvenTreePart?> {
|
||||
|
||||
final partSearchKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
BuildContext context;
|
||||
|
||||
// What did we search for last time?
|
||||
String _cachedQuery = "";
|
||||
|
||||
bool _searching = false;
|
||||
|
||||
// Custom filters for the part search
|
||||
Map<String, String> _filters = {};
|
||||
|
||||
PartSearchDelegate(this.context, {Map<String, String> filters = const {}}) {
|
||||
|
||||
// Copy filter values
|
||||
for (String key in filters.keys) {
|
||||
|
||||
String? value = filters[key];
|
||||
|
||||
if (value != null) {
|
||||
_filters[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Widget for performing database-wide search
|
||||
class SearchWidget extends StatefulWidget {
|
||||
|
||||
@override
|
||||
String get searchFieldLabel => L10().searchParts;
|
||||
_SearchDisplayState createState() => _SearchDisplayState();
|
||||
|
||||
// List of part results
|
||||
List<InvenTreePart> partResults = [];
|
||||
|
||||
Future<void> search(BuildContext context) async {
|
||||
|
||||
// Search string too short!
|
||||
if (query.length < 3) {
|
||||
partResults.clear();
|
||||
showResults(context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (query == _cachedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
_cachedQuery = query;
|
||||
|
||||
_searching = true;
|
||||
|
||||
print("Searching...");
|
||||
|
||||
showResults(context);
|
||||
|
||||
_filters["cascade"] = "true";
|
||||
|
||||
final results = await InvenTreePart().search(context, query, filters: _filters);
|
||||
|
||||
partResults.clear();
|
||||
|
||||
for (int idx = 0; idx < results.length; idx++) {
|
||||
if (results[idx] is InvenTreePart) {
|
||||
partResults.add(results[idx] as InvenTreePart);
|
||||
}
|
||||
}
|
||||
|
||||
print("Searching complete! Results: ${partResults.length}");
|
||||
_searching = false;
|
||||
|
||||
showSnackIcon(
|
||||
"${partResults.length} ${L10().results}",
|
||||
success: partResults.length > 0,
|
||||
icon: FontAwesomeIcons.pollH,
|
||||
);
|
||||
|
||||
// For some reason, need to toggle between suggestions and results here...
|
||||
showSuggestions(context);
|
||||
showResults(context);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.backspace),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
search(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.search),
|
||||
onPressed: () {
|
||||
search(context);
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
this.close(context, null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _partResult(BuildContext context, int index) {
|
||||
|
||||
InvenTreePart part = partResults[index];
|
||||
|
||||
return ListTile(
|
||||
title: Text(part.fullname),
|
||||
subtitle: Text(part.description),
|
||||
leading: InvenTreeAPI().getImage(
|
||||
part.thumbnail,
|
||||
width: 40,
|
||||
height: 40
|
||||
),
|
||||
trailing: Text(part.inStockString),
|
||||
onTap: () {
|
||||
InvenTreePart().get(part.pk).then((var prt) {
|
||||
if (prt is InvenTreePart) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => PartDetailWidget(prt))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
|
||||
print("build results");
|
||||
|
||||
if (_searching) {
|
||||
return progressIndicator();
|
||||
}
|
||||
|
||||
search(context);
|
||||
|
||||
if (query.length == 0) {
|
||||
return ListTile(
|
||||
title: Text(L10().queryEnter)
|
||||
);
|
||||
}
|
||||
|
||||
if (query.length < 3) {
|
||||
return ListTile(
|
||||
title: Text(L10().queryShort),
|
||||
subtitle: Text(L10().queryShortDetail)
|
||||
);
|
||||
}
|
||||
|
||||
if (partResults.length == 0) {
|
||||
return ListTile(
|
||||
title: Text(L10().noResults),
|
||||
subtitle: Text(L10().queryNoResults + " '${query}'")
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: ClampingScrollPhysics(),
|
||||
separatorBuilder: (_, __) => const Divider(height: 3),
|
||||
itemBuilder: _partResult,
|
||||
itemCount: partResults.length,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
// TODO - Implement
|
||||
return Column();
|
||||
}
|
||||
|
||||
// Ensure the search theme matches the app theme
|
||||
@override
|
||||
ThemeData appBarTheme(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem?> {
|
||||
|
||||
final stockSearchKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
String _cachedQuery = "";
|
||||
|
||||
bool _searching = false;
|
||||
|
||||
// Custom filters for the stock item search
|
||||
Map<String, String> _filters = {};
|
||||
|
||||
StockSearchDelegate(this.context, {Map<String, String> filters = const {}}) {
|
||||
|
||||
// Copy filter values
|
||||
for (String key in filters.keys) {
|
||||
|
||||
String? value = filters[key];
|
||||
|
||||
if (value != null) {
|
||||
_filters[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
class _SearchDisplayState extends RefreshableState<SearchWidget> {
|
||||
|
||||
@override
|
||||
String get searchFieldLabel => L10().searchStock;
|
||||
String getAppBarTitle(BuildContext context) => L10().search;
|
||||
|
||||
// List of StockItem results
|
||||
List<InvenTreeStockItem> itemResults = [];
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
Timer? debounceTimer;
|
||||
|
||||
int nPartResults = 0;
|
||||
|
||||
int nCategoryResults = 0;
|
||||
|
||||
int nStockResults = 0;
|
||||
|
||||
int nLocationResults = 0;
|
||||
|
||||
int nSupplierResults = 0;
|
||||
|
||||
int nPurchaseOrderResults = 0;
|
||||
|
||||
// Callback when the text is being edited
|
||||
// Incorporates a debounce timer to restrict search frequency
|
||||
void onSearchTextChanged(String text, {bool immediate = false}) {
|
||||
|
||||
if (debounceTimer?.isActive ?? false) {
|
||||
debounceTimer!.cancel();
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
search(text);
|
||||
} else {
|
||||
debounceTimer = Timer(Duration(milliseconds: 250), () {
|
||||
search(text);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Future<void> search(String term) async {
|
||||
|
||||
if (term.isEmpty) {
|
||||
setState(() {
|
||||
// Do not search on an empty string
|
||||
nPartResults = 0;
|
||||
nCategoryResults = 0;
|
||||
nStockResults = 0;
|
||||
nLocationResults = 0;
|
||||
nSupplierResults = 0;
|
||||
nPurchaseOrderResults = 0;
|
||||
});
|
||||
|
||||
Future<void> search(BuildContext context) async {
|
||||
// Search string too short!
|
||||
if (query.length < 3) {
|
||||
itemResults.clear();
|
||||
showResults(context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (query == _cachedQuery) {
|
||||
return;
|
||||
}
|
||||
// Search parts
|
||||
InvenTreePart().count(
|
||||
searchQuery: term
|
||||
).then((int n) {
|
||||
setState(() {
|
||||
nPartResults = n;
|
||||
});
|
||||
});
|
||||
|
||||
_cachedQuery = query;
|
||||
// Search part categories
|
||||
InvenTreePartCategory().count(
|
||||
searchQuery: term,
|
||||
).then((int n) {
|
||||
setState(() {
|
||||
nCategoryResults = n;
|
||||
});
|
||||
});
|
||||
|
||||
_searching = true;
|
||||
// Search stock items
|
||||
InvenTreeStockItem().count(
|
||||
searchQuery: term
|
||||
).then((int n) {
|
||||
setState(() {
|
||||
nStockResults = n;
|
||||
});
|
||||
});
|
||||
|
||||
print("Searching...");
|
||||
// Search stock locations
|
||||
InvenTreeStockLocation().count(
|
||||
searchQuery: term
|
||||
).then((int n) {
|
||||
setState(() {
|
||||
nLocationResults = n;
|
||||
});
|
||||
});
|
||||
|
||||
showResults(context);
|
||||
// Search suppliers
|
||||
InvenTreeCompany().count(
|
||||
searchQuery: term,
|
||||
filters: {
|
||||
"is_supplier": "true",
|
||||
},
|
||||
).then((int n) {
|
||||
setState(() {
|
||||
nSupplierResults = n;
|
||||
});
|
||||
});
|
||||
|
||||
// Enable cascading part search by default
|
||||
_filters["cascade"] = "true";
|
||||
|
||||
final results = await InvenTreeStockItem().search(
|
||||
context, query, filters: _filters);
|
||||
|
||||
itemResults.clear();
|
||||
|
||||
for (int idx = 0; idx < results.length; idx++) {
|
||||
if (results[idx] is InvenTreeStockItem) {
|
||||
itemResults.add(results[idx] as InvenTreeStockItem);
|
||||
// Search purchase orders
|
||||
InvenTreePurchaseOrder().count(
|
||||
searchQuery: term,
|
||||
filters: {
|
||||
"outstanding": "true"
|
||||
}
|
||||
}
|
||||
).then((int n) {
|
||||
setState(() {
|
||||
nPurchaseOrderResults = n;
|
||||
});
|
||||
});
|
||||
|
||||
_searching = false;
|
||||
}
|
||||
|
||||
showSnackIcon(
|
||||
"${itemResults.length} ${L10().results}",
|
||||
success: itemResults.length > 0,
|
||||
icon: FontAwesomeIcons.pollH,
|
||||
List<Widget> _tiles(BuildContext context) {
|
||||
|
||||
List<Widget> tiles = [];
|
||||
|
||||
// Search input
|
||||
tiles.add(
|
||||
InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
),
|
||||
child: ListTile(
|
||||
title: TextField(
|
||||
readOnly: false,
|
||||
controller: searchController,
|
||||
onChanged: (String text) {
|
||||
onSearchTextChanged(text);
|
||||
},
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.backspace, color: Colors.red),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
onSearchTextChanged("", immediate: true);
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
showSuggestions(context);
|
||||
showResults(context);
|
||||
}
|
||||
String query = searchController.text;
|
||||
|
||||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.backspace),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
search(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.search),
|
||||
onPressed: () {
|
||||
search(context);
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
List<Widget> results = [];
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
this.close(context, null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _itemResult(BuildContext context, int index) {
|
||||
|
||||
InvenTreeStockItem item = itemResults[index];
|
||||
|
||||
return ListTile(
|
||||
title: Text(item.partName),
|
||||
subtitle: Text(item.locationName),
|
||||
leading: InvenTreeAPI().getImage(
|
||||
item.partThumbnail,
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
trailing: Text(item.serialOrQuantityDisplay()),
|
||||
onTap: () {
|
||||
InvenTreeStockItem().get(item.pk).then((var it) {
|
||||
if (it is InvenTreeStockItem) {
|
||||
// Part Results
|
||||
if (nPartResults > 0) {
|
||||
results.add(
|
||||
ListTile(
|
||||
title: Text(L10().parts),
|
||||
leading: FaIcon(FontAwesomeIcons.shapes),
|
||||
trailing: Text("${nPartResults}"),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => StockDetailWidget(it))
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PartList(
|
||||
{
|
||||
"original_search": query
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Part Category Results
|
||||
if (nCategoryResults > 0) {
|
||||
results.add(
|
||||
ListTile(
|
||||
title: Text(L10().partCategories),
|
||||
leading: FaIcon(FontAwesomeIcons.sitemap),
|
||||
trailing: Text("${nCategoryResults}"),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PartCategoryList(
|
||||
{
|
||||
"original_search": query
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Stock Item Results
|
||||
if (nStockResults > 0) {
|
||||
results.add(
|
||||
ListTile(
|
||||
title: Text(L10().stockItems),
|
||||
leading: FaIcon(FontAwesomeIcons.boxes),
|
||||
trailing: Text("${nStockResults}"),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StockItemList(
|
||||
{
|
||||
"original_search": query,
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Stock location results
|
||||
if (nLocationResults > 0) {
|
||||
results.add(
|
||||
ListTile(
|
||||
title: Text(L10().stockLocations),
|
||||
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
|
||||
trailing: Text("${nLocationResults}"),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StockLocationList(
|
||||
{
|
||||
"original_search": query
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Suppliers
|
||||
if (nSupplierResults > 0) {
|
||||
results.add(
|
||||
ListTile(
|
||||
title: Text(L10().suppliers),
|
||||
leading: FaIcon(FontAwesomeIcons.building),
|
||||
trailing: Text("${nSupplierResults}"),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CompanyListWidget(
|
||||
L10().suppliers,
|
||||
{
|
||||
"is_supplier": "true",
|
||||
"original_search": query
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Purchase orders
|
||||
if (nPurchaseOrderResults > 0) {
|
||||
results.add(
|
||||
ListTile(
|
||||
title: Text(L10().purchaseOrders),
|
||||
leading: FaIcon(FontAwesomeIcons.shoppingCart),
|
||||
trailing: Text("${nPurchaseOrderResults}"),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PurchaseOrderListWidget(
|
||||
filters: {
|
||||
"original_search": query
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (results.isEmpty) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().queryNoResults),
|
||||
leading: FaIcon(FontAwesomeIcons.search),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
for (Widget result in results) {
|
||||
tiles.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
return tiles;
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget getBody(BuildContext context) {
|
||||
return Center(
|
||||
child: ListView(
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: _tiles(context),
|
||||
).toList()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
|
||||
search(context);
|
||||
|
||||
if (_searching) {
|
||||
return progressIndicator();
|
||||
}
|
||||
|
||||
search(context);
|
||||
|
||||
if (query.length == 0) {
|
||||
return ListTile(
|
||||
title: Text(L10().queryEnter)
|
||||
);
|
||||
}
|
||||
|
||||
if (query.length < 3) {
|
||||
return ListTile(
|
||||
title: Text(L10().queryShort),
|
||||
subtitle: Text(L10().queryShortDetail)
|
||||
);
|
||||
}
|
||||
|
||||
if (itemResults.length == 0) {
|
||||
return ListTile(
|
||||
title: Text(L10().noResults),
|
||||
subtitle: Text(L10().queryNoResults + " '${query}'")
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: ClampingScrollPhysics(),
|
||||
separatorBuilder: (_, __) => const Divider(height: 3),
|
||||
itemBuilder: _itemResult,
|
||||
itemCount: itemResults.length,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
// TODO - Implement
|
||||
return Column();
|
||||
}
|
||||
|
||||
// Ensure the search theme matches the app theme
|
||||
@override
|
||||
ThemeData appBarTheme(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,16 +8,20 @@
|
||||
* | Text <icon> |
|
||||
*/
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:one_context/one_context.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
|
||||
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
|
||||
|
||||
OneContext().hideCurrentSnackBar();
|
||||
BuildContext? context = OneContext().context;
|
||||
|
||||
if (context != null) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
}
|
||||
|
||||
Color backgroundColor = Colors.deepOrange;
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
|
||||
class Spinner extends StatefulWidget {
|
||||
final IconData? icon;
|
||||
final Duration duration;
|
||||
final Color color;
|
||||
|
||||
const Spinner({
|
||||
this.color = COLOR_GRAY_LIGHT,
|
||||
@ -16,12 +13,16 @@ class Spinner extends StatefulWidget {
|
||||
this.duration = const Duration(milliseconds: 1800),
|
||||
}) : super(key: key);
|
||||
|
||||
final IconData? icon;
|
||||
final Duration duration;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
_SpinnerState createState() => _SpinnerState();
|
||||
}
|
||||
|
||||
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
|
||||
AnimationController? _controller;
|
||||
late AnimationController? _controller;
|
||||
Widget? _child;
|
||||
|
||||
@override
|
||||
|
@ -1,20 +1,18 @@
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/part_detail.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/part_detail.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import '../api.dart';
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
|
||||
class StarredPartWidget extends StatefulWidget {
|
||||
|
||||
StarredPartWidget({Key? key}) : super(key: key);
|
||||
const StarredPartWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_StarredPartState createState() => _StarredPartState();
|
||||
@ -73,7 +71,7 @@ class _StarredPartState extends RefreshableState<StarredPartWidget> {
|
||||
return progressIndicator();
|
||||
}
|
||||
|
||||
if (starredParts.length == 0) {
|
||||
if (starredParts.isEmpty) {
|
||||
return ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
|
@ -1,30 +1,30 @@
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/barcode.dart';
|
||||
import 'package:inventree/inventree/model.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/widget/location_display.dart';
|
||||
import 'package:inventree/widget/part_detail.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import 'package:inventree/widget/stock_item_test_results.dart';
|
||||
import 'package:inventree/widget/stock_notes.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/barcode.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/fields.dart";
|
||||
import "package:inventree/widget/location_display.dart";
|
||||
import "package:inventree/widget/part_detail.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:inventree/widget/stock_item_test_results.dart";
|
||||
import "package:inventree/widget/stock_notes.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
|
||||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import "package:dropdown_search/dropdown_search.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
class StockDetailWidget extends StatefulWidget {
|
||||
|
||||
StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
||||
const StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
@ -35,6 +35,8 @@ class StockDetailWidget extends StatefulWidget {
|
||||
|
||||
class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
|
||||
_StockItemDisplayState(this.item);
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().stockItem;
|
||||
|
||||
@ -46,14 +48,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
final _countStockKey = GlobalKey<FormState>();
|
||||
final _moveStockKey = GlobalKey<FormState>();
|
||||
|
||||
_StockItemDisplayState(this.item);
|
||||
|
||||
@override
|
||||
List<Widget> getAppBarActions(BuildContext context) {
|
||||
|
||||
List<Widget> actions = [];
|
||||
|
||||
if (InvenTreeAPI().checkPermission('stock', 'view')) {
|
||||
if (InvenTreeAPI().checkPermission("stock", "view")) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.globe),
|
||||
@ -62,7 +62,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
||||
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
@ -99,13 +99,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
await item.reload();
|
||||
|
||||
// Request part information
|
||||
part = await InvenTreePart().get(item.partId) as InvenTreePart;
|
||||
part = await InvenTreePart().get(item.partId) as InvenTreePart?;
|
||||
|
||||
// Request test results...
|
||||
await item.getTestResults();
|
||||
}
|
||||
|
||||
void _editStockItem(BuildContext context) async {
|
||||
Future <void> _editStockItem(BuildContext context) async {
|
||||
|
||||
var fields = InvenTreeStockItem().formFields();
|
||||
|
||||
@ -125,7 +125,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
|
||||
}
|
||||
|
||||
void _addStock() async {
|
||||
Future <void> _addStock() async {
|
||||
|
||||
double quantity = double.parse(_quantityController.text);
|
||||
_quantityController.clear();
|
||||
@ -138,7 +138,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
refresh();
|
||||
}
|
||||
|
||||
void _addStockDialog() async {
|
||||
Future <void> _addStockDialog() async {
|
||||
|
||||
_quantityController.clear();
|
||||
_notesController.clear();
|
||||
@ -171,7 +171,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
void _removeStock() async {
|
||||
Future <void> _removeStock() async {
|
||||
|
||||
double quantity = double.parse(_quantityController.text);
|
||||
_quantityController.clear();
|
||||
@ -211,7 +211,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
void _countStock() async {
|
||||
Future <void> _countStock() async {
|
||||
|
||||
double quantity = double.parse(_quantityController.text);
|
||||
_quantityController.clear();
|
||||
@ -223,9 +223,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
refresh();
|
||||
}
|
||||
|
||||
void _countStockDialog() async {
|
||||
Future <void> _countStockDialog() async {
|
||||
|
||||
_quantityController.text = item.quantityString;
|
||||
_quantityController.text = item.quantity.toString();
|
||||
_notesController.clear();
|
||||
|
||||
showFormDialog(L10().countStock,
|
||||
@ -251,9 +251,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
}
|
||||
|
||||
|
||||
void _unassignBarcode(BuildContext context) async {
|
||||
Future<void> _unassignBarcode(BuildContext context) async {
|
||||
|
||||
final bool result = await item.update(values: {'uid': ''});
|
||||
final bool result = await item.update(values: {"uid": ""});
|
||||
|
||||
if (result) {
|
||||
showSnackIcon(
|
||||
@ -271,7 +271,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
}
|
||||
|
||||
|
||||
void _transferStock(int locationId) async {
|
||||
Future <void> _transferStock(int locationId) async {
|
||||
|
||||
double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
|
||||
String notes = _notesController.text;
|
||||
@ -288,11 +288,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
void _transferStockDialog(BuildContext context) async {
|
||||
Future <void> _transferStockDialog(BuildContext context) async {
|
||||
|
||||
int? location_pk;
|
||||
|
||||
_quantityController.text = "${item.quantityString}";
|
||||
_quantityController.text = "${item.quantity}";
|
||||
|
||||
showFormDialog(L10().transferStock,
|
||||
key: _moveStockKey,
|
||||
@ -327,13 +327,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
},
|
||||
onFind: (String filter) async {
|
||||
|
||||
Map<String, String> _filters = {
|
||||
"search": filter,
|
||||
"offset": "0",
|
||||
"limit": "25"
|
||||
};
|
||||
|
||||
final List<InvenTreeModel> results = await InvenTreeStockLocation().list(filters: _filters);
|
||||
final results = await InvenTreeStockLocation().search(filter);
|
||||
|
||||
List<dynamic> items = [];
|
||||
|
||||
@ -349,13 +343,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
hint: L10().searchLocation,
|
||||
onChanged: null,
|
||||
itemAsString: (dynamic location) {
|
||||
return location['pathstring'];
|
||||
return (location["pathstring"] ?? "") as String;
|
||||
},
|
||||
onSaved: (dynamic location) {
|
||||
if (location == null) {
|
||||
location_pk = null;
|
||||
} else {
|
||||
location_pk = location['pk'];
|
||||
location_pk = location["pk"] as int;
|
||||
}
|
||||
},
|
||||
isFilteredOnline: true,
|
||||
@ -420,7 +414,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
ListTile(
|
||||
title: Text(L10().quantity),
|
||||
leading: FaIcon(FontAwesomeIcons.cubes),
|
||||
trailing: Text("${item.quantityString}"),
|
||||
trailing: Text("${item.quantityString()}"),
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -503,7 +497,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
|
||||
// Supplier part?
|
||||
// TODO: Display supplier part info page?
|
||||
if (false && item.supplierPartId > 0) {
|
||||
/*
|
||||
if (item.supplierPartId > 0) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text("${item.supplierName}"),
|
||||
@ -514,6 +509,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
)
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
if (item.link.isNotEmpty) {
|
||||
tiles.add(
|
||||
@ -559,7 +555,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
// TODO - Is this stock item linked to a PurchaseOrder?
|
||||
|
||||
// TODO - Re-enable stock item history display
|
||||
if (false && item.trackingItemCount > 0) {
|
||||
/*
|
||||
if (item.trackingItemCount > 0) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().history),
|
||||
@ -574,6 +571,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
)
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
// Notes field
|
||||
tiles.add(
|
||||
@ -600,7 +598,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(headerTile());
|
||||
|
||||
// First check that the user has the required permissions to adjust stock
|
||||
if (!InvenTreeAPI().checkPermission('stock', 'change')) {
|
||||
if (!InvenTreeAPI().checkPermission("stock", "change")) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().permissionRequired),
|
||||
@ -624,7 +622,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
title: Text(L10().countStock),
|
||||
leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK),
|
||||
onTap: _countStockDialog,
|
||||
trailing: Text(item.quantityString),
|
||||
trailing: Text(item.quantityString(includeUnits: true)),
|
||||
)
|
||||
);
|
||||
|
||||
@ -678,12 +676,31 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
|
||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemBarcodeAssignmentHandler(item)))
|
||||
).then((context) {
|
||||
refresh();
|
||||
|
||||
var handler = UniqueBarcodeHandler((String hash) {
|
||||
item.update(
|
||||
values: {
|
||||
"uid": hash,
|
||||
}
|
||||
).then((result) {
|
||||
if (result) {
|
||||
successTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeAssigned,
|
||||
success: true,
|
||||
icon: FontAwesomeIcons.qrcode
|
||||
);
|
||||
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => InvenTreeQRView(handler))
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
@ -710,12 +727,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
items: <BottomNavigationBarItem> [
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.infoCircle),
|
||||
title: Text(L10().details),
|
||||
label: L10().details,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.wrench),
|
||||
title: Text(L10().actions),
|
||||
),
|
||||
label: L10().actions, ),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -1,27 +1,21 @@
|
||||
import 'package:inventree/api_form.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/inventree/model.dart';
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
|
||||
class StockItemTestResultsWidget extends StatefulWidget {
|
||||
|
||||
StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
|
||||
const StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
@ -32,7 +26,7 @@ class StockItemTestResultsWidget extends StatefulWidget {
|
||||
|
||||
class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestResultsWidget> {
|
||||
|
||||
final _addResultKey = GlobalKey<FormState>();
|
||||
_StockItemTestResultDisplayState(this.item);
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().testResults;
|
||||
@ -57,9 +51,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
_StockItemTestResultDisplayState(this.item);
|
||||
|
||||
void addTestResult(BuildContext context, {String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
|
||||
Future <void> addTestResult(BuildContext context, {String name = "", bool nameIsEditable = true, bool result = false, String value = "", bool valueRequired = false, bool attachmentRequired = false}) async {
|
||||
|
||||
InvenTreeStockItemTestResult().createForm(
|
||||
context,
|
||||
@ -150,7 +142,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
|
||||
var results = getTestResults();
|
||||
|
||||
if (results.length == 0) {
|
||||
if (results.isEmpty) {
|
||||
tiles.add(ListTile(
|
||||
title: Text(L10().testResultNone),
|
||||
subtitle: Text(L10().testResultNoneDetail),
|
||||
@ -165,7 +157,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
String _test = "";
|
||||
bool _result = false;
|
||||
String _value = "";
|
||||
String _notes = "";
|
||||
|
||||
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
|
||||
bool _valueRequired = false;
|
||||
@ -175,8 +166,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
_result = item.passFailStatus();
|
||||
_test = item.testName;
|
||||
_required = item.required;
|
||||
_value = item.latestResult()?.value ?? '';
|
||||
_notes = item.latestResult()?.notes ?? '';
|
||||
_value = item.latestResult()?.value ?? "";
|
||||
_valueRequired = item.requiresValue;
|
||||
_attachmentRequired = item.requiresAttachment;
|
||||
} else if (item is InvenTreeStockItemTestResult) {
|
||||
@ -184,7 +174,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
_test = item.testName;
|
||||
_required = false;
|
||||
_value = item.value;
|
||||
_notes = item.notes;
|
||||
}
|
||||
|
||||
if (_result == true) {
|
||||
|
105
lib/widget/stock_list.dart
Normal file
105
lib/widget/stock_list.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter_markdown/flutter_markdown.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import '../api.dart';
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
|
||||
class StockNotesWidget extends StatefulWidget {
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
const StockNotesWidget(this.item, {Key? key}) : super(key: key);
|
||||
|
||||
StockNotesWidget(this.item, {Key? key}) : super(key: key);
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
@override
|
||||
_StockNotesState createState() => _StockNotesState(item);
|
||||
@ -23,10 +23,10 @@ class StockNotesWidget extends StatefulWidget {
|
||||
|
||||
class _StockNotesState extends RefreshableState<StockNotesWidget> {
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
_StockNotesState(this.item);
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().stockItemNotes;
|
||||
|
||||
@ -39,7 +39,7 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> {
|
||||
List<Widget> getAppBarActions(BuildContext context) {
|
||||
List<Widget> actions = [];
|
||||
|
||||
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
||||
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
|
@ -1,12 +1,10 @@
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
|
||||
import '../l10.dart';
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
class SubmitFeedbackWidget extends StatefulWidget {
|
||||
|
||||
@ -18,7 +16,7 @@ class SubmitFeedbackWidget extends StatefulWidget {
|
||||
|
||||
class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
|
||||
|
||||
final _formkey = new GlobalKey<FormState>();
|
||||
final _formkey = GlobalKey<FormState>();
|
||||
|
||||
String message = "";
|
||||
|
||||
@ -61,8 +59,6 @@ class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
|
||||
key: _formkey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
|
30
pubspec.lock
30
pubspec.lock
@ -49,7 +49,21 @@ packages:
|
||||
name: cached_network_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.1.0"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
camera:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -113,6 +127,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
datetime_picker_formfield:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: datetime_picker_formfield
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -315,6 +336,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
lint:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lint
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
45
pubspec.yaml
45
pubspec.yaml
@ -13,40 +13,43 @@ environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
|
||||
audioplayers: ^0.20.1 # Play audio files
|
||||
cached_network_image: ^3.1.0 # Download and cache remote images
|
||||
camera: # Camera
|
||||
cupertino_icons: ^1.0.3
|
||||
datetime_picker_formfield: ^2.0.0 # Date / time picker
|
||||
device_info_plus: ^2.1.0 # Information about the device
|
||||
dropdown_search: 0.6.3 # Dropdown autocomplete form fields
|
||||
file_picker: ^4.0.0 # Select files from the device
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
intl: ^0.17.0
|
||||
|
||||
cupertino_icons: ^1.0.3
|
||||
http: ^0.13.0
|
||||
cached_network_image: ^3.0.0 # Download and cache remote images
|
||||
qr_code_scanner: ^0.5.2 # Barcode scanning
|
||||
package_info_plus: ^1.0.4 # App information introspection
|
||||
device_info_plus: ^2.1.0 # Information about the device
|
||||
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
||||
sentry_flutter: 5.0.0 # Error reporting
|
||||
image_picker: ^0.8.3 # Select or take photos
|
||||
file_picker: ^4.0.0 # Select files from the device
|
||||
url_launcher: 6.0.9 # Open link in system browser
|
||||
open_file: 3.2.1 # Open local files
|
||||
flutter_markdown: ^0.6.2 # Rendering markdown
|
||||
camera: # Camera
|
||||
path_provider: 2.0.2 # Local file storage
|
||||
sembast: ^3.1.0+2 # NoSQL data storage
|
||||
one_context: ^1.1.0 # Dialogs without requiring context
|
||||
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
||||
http: ^0.13.0
|
||||
image_picker: ^0.8.3 # Select or take photos
|
||||
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
|
||||
audioplayers: ^0.20.1 # Play audio files
|
||||
dropdown_search: 0.6.3 # Dropdown autocomplete form fields
|
||||
intl: ^0.17.0
|
||||
one_context: ^1.1.0 # Dialogs without requiring context
|
||||
open_file: 3.2.1 # Open local files
|
||||
package_info_plus: ^1.0.4 # App information introspection
|
||||
path:
|
||||
path_provider: 2.0.2 # Local file storage
|
||||
qr_code_scanner: ^0.5.2 # Barcode scanning
|
||||
sembast: ^3.1.0+2 # NoSQL data storage
|
||||
sentry_flutter: 5.0.0 # Error reporting
|
||||
url_launcher: 6.0.9 # Open link in system browser
|
||||
|
||||
dev_dependencies:
|
||||
flutter_launcher_icons:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_launcher_icons:
|
||||
lint: ^1.0.0
|
||||
|
||||
flutter_icons:
|
||||
android: true
|
||||
|
@ -5,9 +5,9 @@
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
testWidgets("Counter increments smoke test", (WidgetTester tester) async {
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user