mirror of
https://github.com/inventree/inventree-app.git
synced 2025-05-03 15:58:54 +00:00
Merge pull request #72 from SchrodingersGat/purchase-orders
Purchase orders
This commit is contained in:
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
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
|
|
||||||
|
coverage/*
|
||||||
|
|
||||||
# Sentry API key
|
# Sentry API key
|
||||||
lib/dsn.dart
|
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 {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.2.0'
|
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
@ -1,6 +1,15 @@
|
|||||||
## InvenTree App Release Notes
|
## InvenTree App Release Notes
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 0.5.0 - October 2021
|
||||||
|
---
|
||||||
|
|
||||||
|
- Display Purchase Order details
|
||||||
|
- Edit Purchase Order information
|
||||||
|
- Display Company details (supplier / manufacturer / customer)
|
||||||
|
- Edit Company information
|
||||||
|
- Fixed bug relating to stock transfer for parts with specified "units"
|
||||||
|
|
||||||
### 0.4.7 - September 2021
|
### 0.4.7 - September 2021
|
||||||
---
|
---
|
||||||
|
|
||||||
|
227
lib/api.dart
227
lib/api.dart
@ -1,24 +1,25 @@
|
|||||||
import 'dart:async';
|
import "dart:async";
|
||||||
import 'dart:convert';
|
import "dart:convert";
|
||||||
import 'dart:io';
|
import "dart:io";
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import "package:flutter/foundation.dart";
|
||||||
import 'package:http/http.dart' as http;
|
import "package:http/http.dart" as http;
|
||||||
import 'package:intl/intl.dart';
|
import "package:intl/intl.dart";
|
||||||
|
import "package:inventree/app_colors.dart";
|
||||||
|
|
||||||
import 'package:open_file/open_file.dart';
|
import "package:open_file/open_file.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import "package:flutter_cache_manager/flutter_cache_manager.dart";
|
||||||
|
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
import "package:inventree/widget/dialogs.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
import "package:inventree/inventree/sentry.dart";
|
||||||
import 'package:inventree/user_profile.dart';
|
import "package:inventree/user_profile.dart";
|
||||||
import 'package:inventree/widget/snacks.dart';
|
import "package:inventree/widget/snacks.dart";
|
||||||
import 'package:path_provider/path_provider.dart';
|
import "package:path_provider/path_provider.dart";
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -49,7 +50,32 @@ class APIResponse {
|
|||||||
|
|
||||||
bool clientError() => (statusCode >= 400) && (statusCode < 500);
|
bool clientError() => (statusCode >= 400) && (statusCode < 500);
|
||||||
|
|
||||||
bool serverError() => (statusCode >= 500);
|
bool serverError() => statusCode >= 500;
|
||||||
|
|
||||||
|
bool isMap() {
|
||||||
|
return data != null && data is Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> asMap() {
|
||||||
|
if (isMap()) {
|
||||||
|
return data as Map<String, dynamic>;
|
||||||
|
} else {
|
||||||
|
// Empty map
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isList() {
|
||||||
|
return data != null && data is List<dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<dynamic> asList() {
|
||||||
|
if (isList()) {
|
||||||
|
return data as List<dynamic>;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -60,8 +86,6 @@ class APIResponse {
|
|||||||
*/
|
*/
|
||||||
class InvenTreeFileService extends FileService {
|
class InvenTreeFileService extends FileService {
|
||||||
|
|
||||||
HttpClient? _client;
|
|
||||||
|
|
||||||
InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
|
InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
|
||||||
_client = client ?? HttpClient();
|
_client = client ?? HttpClient();
|
||||||
|
|
||||||
@ -73,6 +97,8 @@ class InvenTreeFileService extends FileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HttpClient? _client;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileServiceResponse> get(String url,
|
Future<FileServiceResponse> get(String url,
|
||||||
{Map<String, String>? headers}) async {
|
{Map<String, String>? headers}) async {
|
||||||
@ -107,6 +133,12 @@ class InvenTreeFileService extends FileService {
|
|||||||
|
|
||||||
class InvenTreeAPI {
|
class InvenTreeAPI {
|
||||||
|
|
||||||
|
factory InvenTreeAPI() {
|
||||||
|
return _api;
|
||||||
|
}
|
||||||
|
|
||||||
|
InvenTreeAPI._internal();
|
||||||
|
|
||||||
// Minimum required API version for server
|
// Minimum required API version for server
|
||||||
static const _minApiVersion = 7;
|
static const _minApiVersion = 7;
|
||||||
|
|
||||||
@ -132,11 +164,12 @@ class InvenTreeAPI {
|
|||||||
String _makeUrl(String url) {
|
String _makeUrl(String url) {
|
||||||
|
|
||||||
// Strip leading slash
|
// Strip leading slash
|
||||||
if (url.startsWith('/')) {
|
if (url.startsWith("/")) {
|
||||||
url = url.substring(1, url.length);
|
url = url.substring(1, url.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
url = url.replaceAll('//', '/');
|
// Prevent double-slash
|
||||||
|
url = url.replaceAll("//", "/");
|
||||||
|
|
||||||
return baseUrl + url;
|
return baseUrl + url;
|
||||||
}
|
}
|
||||||
@ -149,7 +182,7 @@ class InvenTreeAPI {
|
|||||||
if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
|
if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
|
||||||
return _makeUrl(endpoint);
|
return _makeUrl(endpoint);
|
||||||
} else {
|
} else {
|
||||||
return _makeUrl("/api/" + endpoint);
|
return _makeUrl("/api/${endpoint}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,10 +217,10 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Server instance information
|
// Server instance information
|
||||||
String instance = '';
|
String instance = "";
|
||||||
|
|
||||||
// Server version information
|
// Server version information
|
||||||
String _version = '';
|
String _version = "";
|
||||||
|
|
||||||
// API version of the connected server
|
// API version of the connected server
|
||||||
int _apiVersion = 1;
|
int _apiVersion = 1;
|
||||||
@ -209,15 +242,14 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we only ever create a single instance of the API class
|
// Ensure we only ever create a single instance of the API class
|
||||||
static final InvenTreeAPI _api = new InvenTreeAPI._internal();
|
static final InvenTreeAPI _api = InvenTreeAPI._internal();
|
||||||
|
|
||||||
factory InvenTreeAPI() {
|
bool supportPoReceive() {
|
||||||
return _api;
|
|
||||||
|
// API endpoint for receiving purchase order line items was introduced in v12
|
||||||
|
return _apiVersion >= 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
InvenTreeAPI._internal();
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Connect to the remote InvenTree server:
|
* Connect to the remote InvenTree server:
|
||||||
*
|
*
|
||||||
@ -239,15 +271,15 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
if (address.isEmpty || username.isEmpty || password.isEmpty) {
|
if (address.isEmpty || username.isEmpty || password.isEmpty) {
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
"Incomplete profile details",
|
L10().incompleteDetails,
|
||||||
icon: FontAwesomeIcons.exclamationCircle,
|
icon: FontAwesomeIcons.exclamationCircle,
|
||||||
success: false
|
success: false
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!address.endsWith('/')) {
|
if (!address.endsWith("/")) {
|
||||||
address = address + '/';
|
address = address + "/";
|
||||||
}
|
}
|
||||||
/* TODO: Better URL validation
|
/* TODO: Better URL validation
|
||||||
* - If not a valid URL, return error
|
* - If not a valid URL, return error
|
||||||
@ -267,8 +299,10 @@ class InvenTreeAPI {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var data = response.asMap();
|
||||||
|
|
||||||
// We expect certain response from the server
|
// We expect certain response from the server
|
||||||
if (response.data == null || !response.data.containsKey("server") || !response.data.containsKey("version") || !response.data.containsKey("instance")) {
|
if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) {
|
||||||
|
|
||||||
showServerError(
|
showServerError(
|
||||||
L10().missingData,
|
L10().missingData,
|
||||||
@ -279,11 +313,11 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record server information
|
// Record server information
|
||||||
_version = response.data["version"];
|
_version = (data["version"] ?? "") as String;
|
||||||
instance = response.data['instance'] ?? '';
|
instance = (data["instance"] ?? "") as String;
|
||||||
|
|
||||||
// Default API version is 1 if not provided
|
// Default API version is 1 if not provided
|
||||||
_apiVersion = (response.data['apiVersion'] ?? 1) as int;
|
_apiVersion = (data["apiVersion"] ?? 1) as int;
|
||||||
|
|
||||||
if (_apiVersion < _minApiVersion) {
|
if (_apiVersion < _minApiVersion) {
|
||||||
|
|
||||||
@ -332,7 +366,9 @@ class InvenTreeAPI {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data == null || !response.data.containsKey("token")) {
|
data = response.asMap();
|
||||||
|
|
||||||
|
if (!data.containsKey("token")) {
|
||||||
showServerError(
|
showServerError(
|
||||||
L10().tokenMissing,
|
L10().tokenMissing,
|
||||||
L10().tokenMissingFromResponse,
|
L10().tokenMissingFromResponse,
|
||||||
@ -342,7 +378,7 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the received token
|
// Return the received token
|
||||||
_token = response.data["token"];
|
_token = (data["token"] ?? "") as String;
|
||||||
print("Received token - $_token");
|
print("Received token - $_token");
|
||||||
|
|
||||||
// Request user role information
|
// Request user role information
|
||||||
@ -358,7 +394,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
_connected = false;
|
_connected = false;
|
||||||
_connecting = false;
|
_connecting = false;
|
||||||
_token = '';
|
_token = "";
|
||||||
profile = null;
|
profile = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,7 +441,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
// Next we request the permissions assigned to the current user
|
// Next we request the permissions assigned to the current user
|
||||||
// Note: 2021-02-27 this "roles" feature for the API was just introduced.
|
// Note: 2021-02-27 this "roles" feature for the API was just introduced.
|
||||||
// Any 'older' version of the server allows any API method for any logged in user!
|
// Any "older" version of the server allows any API method for any logged in user!
|
||||||
// We will return immediately, but request the user roles in the background
|
// We will return immediately, but request the user roles in the background
|
||||||
|
|
||||||
var response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
|
var response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
|
||||||
@ -414,9 +450,11 @@ class InvenTreeAPI {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data.containsKey('roles')) {
|
var data = response.asMap();
|
||||||
|
|
||||||
|
if (data.containsKey("roles")) {
|
||||||
// Save a local copy of the user roles
|
// Save a local copy of the user roles
|
||||||
roles = response.data['roles'];
|
roles = response.data["roles"] as Map<String, dynamic>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,7 +462,7 @@ class InvenTreeAPI {
|
|||||||
/*
|
/*
|
||||||
* Check if the user has the given role.permission assigned
|
* Check if the user has the given role.permission assigned
|
||||||
*e
|
*e
|
||||||
* e.g. 'part', 'change'
|
* e.g. "part", "change"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// If we do not have enough information, assume permission is allowed
|
// If we do not have enough information, assume permission is allowed
|
||||||
@ -437,7 +475,7 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<String> perms = List.from(roles[role]);
|
List<String> perms = List.from(roles[role] as List<dynamic>);
|
||||||
return perms.contains(permission);
|
return perms.contains(permission);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
sentryReportError(error, stackTrace);
|
sentryReportError(error, stackTrace);
|
||||||
@ -447,19 +485,17 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
|
|
||||||
// Perform a PATCH request
|
// Perform a PATCH request
|
||||||
Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int? expectedStatusCode}) async {
|
Future<APIResponse> patch(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode}) async {
|
||||||
var _body = Map<String, String>();
|
|
||||||
|
|
||||||
// Copy across provided data
|
Map<String, dynamic> _body = body;
|
||||||
body.forEach((K, V) => _body[K] = V);
|
|
||||||
|
|
||||||
HttpClientRequest? request = await apiRequest(url, "PATCH");
|
HttpClientRequest? request = await apiRequest(url, "PATCH");
|
||||||
|
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
// Return an "invalid" APIResponse
|
// Return an "invalid" APIResponse
|
||||||
return new APIResponse(
|
return APIResponse(
|
||||||
url: url,
|
url: url,
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
error: "HttpClientRequest is null"
|
error: "HttpClientRequest is null"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -503,7 +539,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
HttpClientRequest? _request;
|
HttpClientRequest? _request;
|
||||||
|
|
||||||
var client = createClient(true);
|
var client = createClient(allowBadCert: true);
|
||||||
|
|
||||||
// Attempt to open a connection to the server
|
// Attempt to open a connection to the server
|
||||||
try {
|
try {
|
||||||
@ -511,8 +547,8 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
// Set headers
|
// Set headers
|
||||||
_request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
|
_request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
|
||||||
_request.headers.set(HttpHeaders.acceptHeader, 'application/json');
|
_request.headers.set(HttpHeaders.acceptHeader, "application/json");
|
||||||
_request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
|
_request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
|
||||||
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
|
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
|
||||||
|
|
||||||
} on SocketException catch (error) {
|
} on SocketException catch (error) {
|
||||||
@ -550,7 +586,7 @@ class InvenTreeAPI {
|
|||||||
showServerError(L10().connectionRefused, error.toString());
|
showServerError(L10().connectionRefused, error.toString());
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
showTimeoutError();
|
showTimeoutError();
|
||||||
} catch (error, stackTrace) {
|
} catch (error) {
|
||||||
print("Error downloading image:");
|
print("Error downloading image:");
|
||||||
print(error.toString());
|
print(error.toString());
|
||||||
showServerError(L10().downloadError, error.toString());
|
showServerError(L10().downloadError, error.toString());
|
||||||
@ -561,7 +597,7 @@ class InvenTreeAPI {
|
|||||||
* Upload a file to the given URL
|
* Upload a file to the given URL
|
||||||
*/
|
*/
|
||||||
Future<APIResponse> uploadFile(String url, File f,
|
Future<APIResponse> uploadFile(String url, File f,
|
||||||
{String name = "attachment", String method="POST", Map<String, String>? fields}) async {
|
{String name = "attachment", String method="POST", Map<String, dynamic>? fields}) async {
|
||||||
var _url = makeApiUrl(url);
|
var _url = makeApiUrl(url);
|
||||||
|
|
||||||
var request = http.MultipartRequest(method, Uri.parse(_url));
|
var request = http.MultipartRequest(method, Uri.parse(_url));
|
||||||
@ -569,8 +605,13 @@ class InvenTreeAPI {
|
|||||||
request.headers.addAll(defaultHeaders());
|
request.headers.addAll(defaultHeaders());
|
||||||
|
|
||||||
if (fields != null) {
|
if (fields != null) {
|
||||||
fields.forEach((String key, String value) {
|
fields.forEach((String key, dynamic value) {
|
||||||
request.fields[key] = value;
|
|
||||||
|
if (value == null) {
|
||||||
|
request.fields[key] = "";
|
||||||
|
} else {
|
||||||
|
request.fields[key] = value.toString();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,9 +693,9 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
// Return an "invalid" APIResponse
|
// Return an "invalid" APIResponse
|
||||||
return new APIResponse(
|
return APIResponse(
|
||||||
url: url,
|
url: url,
|
||||||
method: 'OPTIONS'
|
method: "OPTIONS"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -671,9 +712,9 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
// Return an "invalid" APIResponse
|
// Return an "invalid" APIResponse
|
||||||
return new APIResponse(
|
return APIResponse(
|
||||||
url: url,
|
url: url,
|
||||||
method: 'POST'
|
method: "POST"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -684,15 +725,13 @@ class InvenTreeAPI {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpClient createClient(bool allowBadCert) {
|
HttpClient createClient({bool allowBadCert = true}) {
|
||||||
|
|
||||||
var client = new HttpClient();
|
var client = HttpClient();
|
||||||
|
|
||||||
client.badCertificateCallback = ((X509Certificate cert, String host, int port) {
|
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
|
||||||
// TODO - Introspection of actual certificate?
|
// TODO - Introspection of actual certificate?
|
||||||
|
|
||||||
allowBadCert = true;
|
|
||||||
|
|
||||||
if (allowBadCert) {
|
if (allowBadCert) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@ -702,7 +741,7 @@ class InvenTreeAPI {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// Set the connection timeout
|
// Set the connection timeout
|
||||||
client.connectionTimeout = Duration(seconds: 30);
|
client.connectionTimeout = Duration(seconds: 30);
|
||||||
@ -714,7 +753,7 @@ class InvenTreeAPI {
|
|||||||
* Initiate a HTTP request to the server
|
* Initiate a HTTP request to the server
|
||||||
*
|
*
|
||||||
* @param url is the API endpoint
|
* @param url is the API endpoint
|
||||||
* @param method is the HTTP method e.g. 'POST' / 'PATCH' / 'GET' etc;
|
* @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
|
||||||
* @param params is the request parameters
|
* @param params is the request parameters
|
||||||
*/
|
*/
|
||||||
Future<HttpClientRequest?> apiRequest(String url, String method, {Map<String, String> urlParams = const {}}) async {
|
Future<HttpClientRequest?> apiRequest(String url, String method, {Map<String, String> urlParams = const {}}) async {
|
||||||
@ -731,7 +770,7 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove extraneous character if present
|
// Remove extraneous character if present
|
||||||
if (_url.endsWith('&')) {
|
if (_url.endsWith("&")) {
|
||||||
_url = _url.substring(0, _url.length - 1);
|
_url = _url.substring(0, _url.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -749,7 +788,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
HttpClientRequest? _request;
|
HttpClientRequest? _request;
|
||||||
|
|
||||||
var client = createClient(true);
|
var client = createClient(allowBadCert: true);
|
||||||
|
|
||||||
// Attempt to open a connection to the server
|
// Attempt to open a connection to the server
|
||||||
try {
|
try {
|
||||||
@ -757,8 +796,8 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
// Set headers
|
// Set headers
|
||||||
_request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
|
_request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
|
||||||
_request.headers.set(HttpHeaders.acceptHeader, 'application/json');
|
_request.headers.set(HttpHeaders.acceptHeader, "application/json");
|
||||||
_request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
|
_request.headers.set(HttpHeaders.contentTypeHeader, "application/json");
|
||||||
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
|
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
|
||||||
|
|
||||||
return _request;
|
return _request;
|
||||||
@ -792,7 +831,7 @@ class InvenTreeAPI {
|
|||||||
request.add(encoded_data);
|
request.add(encoded_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
APIResponse response = new APIResponse(
|
APIResponse response = APIResponse(
|
||||||
method: request.method,
|
method: request.method,
|
||||||
url: request.uri.toString()
|
url: request.uri.toString()
|
||||||
);
|
);
|
||||||
@ -805,18 +844,7 @@ class InvenTreeAPI {
|
|||||||
// If the server returns a server error code, alert the user
|
// If the server returns a server error code, alert the user
|
||||||
if (_response.statusCode >= 500) {
|
if (_response.statusCode >= 500) {
|
||||||
showStatusCodeError(_response.statusCode);
|
showStatusCodeError(_response.statusCode);
|
||||||
} else {
|
|
||||||
response.data = await responseToJson(_response) ?? {};
|
|
||||||
|
|
||||||
if (statusCode != null) {
|
|
||||||
|
|
||||||
// Expected status code not returned
|
|
||||||
if (statusCode != _response.statusCode) {
|
|
||||||
showStatusCodeError(_response.statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report any server errors
|
|
||||||
if (_response.statusCode >= 500) {
|
|
||||||
sentryReportMessage(
|
sentryReportMessage(
|
||||||
"Server error",
|
"Server error",
|
||||||
context: {
|
context: {
|
||||||
@ -828,6 +856,15 @@ class InvenTreeAPI {
|
|||||||
"responseData": response.data.toString(),
|
"responseData": response.data.toString(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
response.data = await responseToJson(_response) ?? {};
|
||||||
|
|
||||||
|
if (statusCode != null) {
|
||||||
|
|
||||||
|
// Expected status code not returned
|
||||||
|
if (statusCode != _response.statusCode) {
|
||||||
|
showStatusCodeError(_response.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -898,9 +935,9 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
// Return an "invalid" APIResponse
|
// Return an "invalid" APIResponse
|
||||||
return new APIResponse(
|
return APIResponse(
|
||||||
url: url,
|
url: url,
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
error: "HttpClientRequest is null",
|
error: "HttpClientRequest is null",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -910,11 +947,11 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
// Return a list of request headers
|
// Return a list of request headers
|
||||||
Map<String, String> defaultHeaders() {
|
Map<String, String> defaultHeaders() {
|
||||||
var headers = Map<String, String>();
|
Map<String, String> headers = {};
|
||||||
|
|
||||||
headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
|
headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
|
||||||
headers[HttpHeaders.acceptHeader] = 'application/json';
|
headers[HttpHeaders.acceptHeader] = "application/json";
|
||||||
headers[HttpHeaders.contentTypeHeader] = 'application/json';
|
headers[HttpHeaders.contentTypeHeader] = "application/json";
|
||||||
headers[HttpHeaders.acceptLanguageHeader] = Intl.getCurrentLocale();
|
headers[HttpHeaders.acceptLanguageHeader] = Intl.getCurrentLocale();
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
@ -924,7 +961,7 @@ class InvenTreeAPI {
|
|||||||
if (_token.isNotEmpty) {
|
if (_token.isNotEmpty) {
|
||||||
return "Token $_token";
|
return "Token $_token";
|
||||||
} else if (profile != null) {
|
} else if (profile != null) {
|
||||||
return "Basic " + base64Encode(utf8.encode('${profile?.username}:${profile?.password}'));
|
return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}"));
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -954,10 +991,10 @@ class InvenTreeAPI {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return new CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
placeholder: (context, url) => CircularProgressIndicator(),
|
placeholder: (context, url) => CircularProgressIndicator(),
|
||||||
errorWidget: (context, url, error) => Icon(FontAwesomeIcons.exclamation),
|
errorWidget: (context, url, error) => FaIcon(FontAwesomeIcons.timesCircle, color: COLOR_DANGER),
|
||||||
httpHeaders: defaultHeaders(),
|
httpHeaders: defaultHeaders(),
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import 'dart:ui';
|
import "dart:ui";
|
||||||
|
|
||||||
const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
|
const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
|
||||||
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
|
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
|
||||||
|
@ -2,14 +2,20 @@
|
|||||||
* Class for managing app-level configuration options
|
* Class for managing app-level configuration options
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:sembast/sembast.dart';
|
import "package:sembast/sembast.dart";
|
||||||
import 'package:inventree/preferences.dart';
|
import "package:inventree/preferences.dart";
|
||||||
|
|
||||||
class InvenTreeSettingsManager {
|
class InvenTreeSettingsManager {
|
||||||
|
|
||||||
|
factory InvenTreeSettingsManager() {
|
||||||
|
return _manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
InvenTreeSettingsManager._internal();
|
||||||
|
|
||||||
final store = StoreRef("settings");
|
final store = StoreRef("settings");
|
||||||
|
|
||||||
Future<Database> get _db async => await InvenTreePreferencesDB.instance.database;
|
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
|
||||||
|
|
||||||
Future<dynamic> getValue(String key, dynamic backup) async {
|
Future<dynamic> getValue(String key, dynamic backup) async {
|
||||||
|
|
||||||
@ -22,17 +28,22 @@ class InvenTreeSettingsManager {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load a boolean setting
|
||||||
|
Future<bool> getBool(String key, bool backup) async {
|
||||||
|
final dynamic value = await getValue(key, backup);
|
||||||
|
|
||||||
|
if (value is bool) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setValue(String key, dynamic value) async {
|
Future<void> setValue(String key, dynamic value) async {
|
||||||
|
|
||||||
await store.record(key).put(await _db, value);
|
await store.record(key).put(await _db, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we only ever create a single instance of this class
|
// Ensure we only ever create a single instance of this class
|
||||||
static final InvenTreeSettingsManager _manager = new InvenTreeSettingsManager._internal();
|
static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal();
|
||||||
|
|
||||||
factory InvenTreeSettingsManager() {
|
|
||||||
return _manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
InvenTreeSettingsManager._internal();
|
|
||||||
}
|
}
|
252
lib/barcode.dart
252
lib/barcode.dart
@ -1,26 +1,24 @@
|
|||||||
import 'package:inventree/app_settings.dart';
|
import "dart:io";
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
|
||||||
import 'package:inventree/widget/snacks.dart';
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:one_context/one_context.dart';
|
|
||||||
|
|
||||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
import "package:inventree/inventree/sentry.dart";
|
||||||
|
import "package:inventree/widget/dialogs.dart";
|
||||||
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
import "package:flutter/cupertino.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
|
import "package:one_context/one_context.dart";
|
||||||
|
|
||||||
import 'package:inventree/inventree/stock.dart';
|
import "package:qr_code_scanner/qr_code_scanner.dart";
|
||||||
import 'package:inventree/inventree/part.dart';
|
|
||||||
import 'package:inventree/l10.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/helpers.dart";
|
||||||
|
import "package:inventree/api.dart";
|
||||||
|
|
||||||
import 'package:inventree/widget/location_display.dart';
|
import "package:inventree/widget/location_display.dart";
|
||||||
import 'package:inventree/widget/part_detail.dart';
|
import "package:inventree/widget/part_detail.dart";
|
||||||
import 'package:inventree/widget/stock_detail.dart';
|
import "package:inventree/widget/stock_detail.dart";
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
|
|
||||||
class BarcodeHandler {
|
class BarcodeHandler {
|
||||||
@ -32,32 +30,12 @@ class BarcodeHandler {
|
|||||||
* based on the response returned from the InvenTree server
|
* based on the response returned from the InvenTree server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
String getOverlayText(BuildContext context) => "Barcode Overlay";
|
|
||||||
|
|
||||||
BarcodeHandler();
|
BarcodeHandler();
|
||||||
|
|
||||||
|
String getOverlayText(BuildContext context) => "Barcode Overlay";
|
||||||
|
|
||||||
QRViewController? _controller;
|
QRViewController? _controller;
|
||||||
|
|
||||||
void successTone() async {
|
|
||||||
|
|
||||||
final bool en = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
|
|
||||||
|
|
||||||
if (en) {
|
|
||||||
final player = AudioCache();
|
|
||||||
player.play("sounds/barcode_scan.mp3");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void failureTone() async {
|
|
||||||
|
|
||||||
final bool en = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
|
|
||||||
|
|
||||||
if (en) {
|
|
||||||
final player = AudioCache();
|
|
||||||
player.play("sounds/barcode_error.mp3");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||||
// Called when the server "matches" a barcode
|
// Called when the server "matches" a barcode
|
||||||
// Override this function
|
// Override this function
|
||||||
@ -101,8 +79,10 @@ class BarcodeHandler {
|
|||||||
|
|
||||||
_controller?.resumeCamera();
|
_controller?.resumeCamera();
|
||||||
|
|
||||||
|
Map<String, dynamic> data = response.asMap();
|
||||||
|
|
||||||
// Handle strange response from the server
|
// Handle strange response from the server
|
||||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
if (!response.isValid() || !response.isMap()) {
|
||||||
onBarcodeUnknown(context, {});
|
onBarcodeUnknown(context, {});
|
||||||
|
|
||||||
// We want to know about this one!
|
// We want to know about this one!
|
||||||
@ -118,12 +98,12 @@ class BarcodeHandler {
|
|||||||
"errorDetail": response.errorDetail,
|
"errorDetail": response.errorDetail,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else if (response.data.containsKey('error')) {
|
} else if (data.containsKey("error")) {
|
||||||
onBarcodeUnknown(context, response.data);
|
onBarcodeUnknown(context, data);
|
||||||
} else if (response.data.containsKey('success')) {
|
} else if (data.containsKey("success")) {
|
||||||
onBarcodeMatched(context, response.data);
|
onBarcodeMatched(context, data);
|
||||||
} else {
|
} else {
|
||||||
onBarcodeUnhandled(context, response.data);
|
onBarcodeUnhandled(context, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,9 +136,9 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
int pk = -1;
|
int pk = -1;
|
||||||
|
|
||||||
// A stocklocation has been passed?
|
// A stocklocation has been passed?
|
||||||
if (data.containsKey('stocklocation')) {
|
if (data.containsKey("stocklocation")) {
|
||||||
|
|
||||||
pk = (data['stocklocation']?['pk'] ?? -1) as int;
|
pk = (data["stocklocation"]?["pk"] ?? -1) as int;
|
||||||
|
|
||||||
if (pk > 0) {
|
if (pk > 0) {
|
||||||
|
|
||||||
@ -180,9 +160,9 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (data.containsKey('stockitem')) {
|
} else if (data.containsKey("stockitem")) {
|
||||||
|
|
||||||
pk = (data['stockitem']?['pk'] ?? -1) as int;
|
pk = (data["stockitem"]?["pk"] ?? -1) as int;
|
||||||
|
|
||||||
if (pk > 0) {
|
if (pk > 0) {
|
||||||
|
|
||||||
@ -206,9 +186,9 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
success: false
|
success: false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (data.containsKey('part')) {
|
} else if (data.containsKey("part")) {
|
||||||
|
|
||||||
pk = (data['part']?['pk'] ?? -1) as int;
|
pk = (data["part"]?["pk"] ?? -1) as int;
|
||||||
|
|
||||||
if (pk > 0) {
|
if (pk > 0) {
|
||||||
|
|
||||||
@ -258,93 +238,24 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
|
|
||||||
/*
|
|
||||||
* Barcode handler for assigning a new barcode to a stock item
|
|
||||||
*/
|
|
||||||
|
|
||||||
final InvenTreeStockItem item;
|
|
||||||
|
|
||||||
StockItemBarcodeAssignmentHandler(this.item);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
|
||||||
|
|
||||||
failureTone();
|
|
||||||
|
|
||||||
// If the barcode is known, we can't assign it to the stock item!
|
|
||||||
showSnackIcon(
|
|
||||||
L10().barcodeInUse,
|
|
||||||
icon: FontAwesomeIcons.qrcode,
|
|
||||||
success: false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
|
||||||
// If the barcode is unknown, we *can* assign it to the stock item!
|
|
||||||
|
|
||||||
if (!data.containsKey("hash")) {
|
|
||||||
showServerError(
|
|
||||||
L10().missingData,
|
|
||||||
L10().barcodeMissingHash,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Send the 'hash' code as the UID for the stock item
|
|
||||||
item.update(
|
|
||||||
values: {
|
|
||||||
"uid": data['hash'],
|
|
||||||
}
|
|
||||||
).then((result) {
|
|
||||||
if (result) {
|
|
||||||
|
|
||||||
failureTone();
|
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
L10().barcodeAssigned,
|
|
||||||
success: true,
|
|
||||||
icon: FontAwesomeIcons.qrcode
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
successTone();
|
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
L10().barcodeNotAssigned,
|
|
||||||
success: false,
|
|
||||||
icon: FontAwesomeIcons.qrcode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StockItemScanIntoLocationHandler extends BarcodeHandler {
|
class StockItemScanIntoLocationHandler extends BarcodeHandler {
|
||||||
/*
|
/*
|
||||||
* Barcode handler for scanning a provided StockItem into a scanned StockLocation
|
* Barcode handler for scanning a provided StockItem into a scanned StockLocation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
final InvenTreeStockItem item;
|
|
||||||
|
|
||||||
StockItemScanIntoLocationHandler(this.item);
|
StockItemScanIntoLocationHandler(this.item);
|
||||||
|
|
||||||
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
|
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||||
// If the barcode points to a 'stocklocation', great!
|
// If the barcode points to a "stocklocation", great!
|
||||||
if (data.containsKey('stocklocation')) {
|
if (data.containsKey("stocklocation")) {
|
||||||
// Extract location information
|
// Extract location information
|
||||||
int location = (data['stocklocation']['pk'] ?? -1) as int;
|
int location = (data["stocklocation"]["pk"] ?? -1) as int;
|
||||||
|
|
||||||
if (location == -1) {
|
if (location == -1) {
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
@ -395,10 +306,10 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
|
|||||||
* Barcode handler for scanning stock item(s) into the specified StockLocation
|
* Barcode handler for scanning stock item(s) into the specified StockLocation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
final InvenTreeStockLocation location;
|
|
||||||
|
|
||||||
StockLocationScanInItemsHandler(this.location);
|
StockLocationScanInItemsHandler(this.location);
|
||||||
|
|
||||||
|
final InvenTreeStockLocation location;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
||||||
|
|
||||||
@ -406,11 +317,11 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
|
|||||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||||
|
|
||||||
// Returned barcode must match a stock item
|
// Returned barcode must match a stock item
|
||||||
if (data.containsKey('stockitem')) {
|
if (data.containsKey("stockitem")) {
|
||||||
|
|
||||||
int item_id = data['stockitem']['pk'] as int;
|
int item_id = data["stockitem"]["pk"] as int;
|
||||||
|
|
||||||
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem;
|
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem?;
|
||||||
|
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
|
|
||||||
@ -462,11 +373,78 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueBarcodeHandler extends BarcodeHandler {
|
||||||
|
/*
|
||||||
|
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
||||||
|
*/
|
||||||
|
|
||||||
|
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
|
||||||
|
|
||||||
|
// Callback function when a "unique" barcode hash is found
|
||||||
|
final Function(String) callback;
|
||||||
|
|
||||||
|
final String overlayText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getOverlayText(BuildContext context) {
|
||||||
|
if (overlayText.isEmpty) {
|
||||||
|
return L10().barcodeScanAssign;
|
||||||
|
} else {
|
||||||
|
return overlayText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||||
|
|
||||||
|
failureTone();
|
||||||
|
|
||||||
|
// If the barcode is known, we can"t assign it to the stock item!
|
||||||
|
showSnackIcon(
|
||||||
|
L10().barcodeInUse,
|
||||||
|
icon: FontAwesomeIcons.qrcode,
|
||||||
|
success: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
||||||
|
// If the barcode is unknown, we *can* assign it to the stock item!
|
||||||
|
|
||||||
|
if (!data.containsKey("hash")) {
|
||||||
|
showServerError(
|
||||||
|
L10().missingData,
|
||||||
|
L10().barcodeMissingHash,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
String hash = (data["hash"] ?? "") as String;
|
||||||
|
|
||||||
|
if (hash.isEmpty) {
|
||||||
|
failureTone();
|
||||||
|
|
||||||
|
showSnackIcon(
|
||||||
|
L10().barcodeError,
|
||||||
|
success: false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
successTone();
|
||||||
|
|
||||||
|
// Close the barcode scanner
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
callback(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeQRView extends StatefulWidget {
|
class InvenTreeQRView extends StatefulWidget {
|
||||||
|
|
||||||
final BarcodeHandler _handler;
|
const InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
|
final BarcodeHandler _handler;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _QRViewState(_handler);
|
State<StatefulWidget> createState() => _QRViewState(_handler);
|
||||||
@ -475,7 +453,9 @@ class InvenTreeQRView extends StatefulWidget {
|
|||||||
|
|
||||||
class _QRViewState extends State<InvenTreeQRView> {
|
class _QRViewState extends State<InvenTreeQRView> {
|
||||||
|
|
||||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
_QRViewState(this._handler) : super();
|
||||||
|
|
||||||
|
final GlobalKey qrKey = GlobalKey(debugLabel: "QR");
|
||||||
|
|
||||||
QRViewController? _controller;
|
QRViewController? _controller;
|
||||||
|
|
||||||
@ -494,8 +474,6 @@ class _QRViewState extends State<InvenTreeQRView> {
|
|||||||
_controller!.resumeCamera();
|
_controller!.resumeCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
_QRViewState(this._handler) : super();
|
|
||||||
|
|
||||||
void _onViewCreated(BuildContext context, QRViewController controller) {
|
void _onViewCreated(BuildContext context, QRViewController controller) {
|
||||||
_controller = controller;
|
_controller = controller;
|
||||||
controller.scannedDataStream.listen((barcode) {
|
controller.scannedDataStream.listen((barcode) {
|
||||||
|
3
lib/dummy_dsn.dart
Normal file
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 {
|
class S implements WidgetsLocalizations {
|
||||||
const S();
|
const S();
|
||||||
|
|
||||||
static const GeneratedLocalizationsDelegate delegate =
|
static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate();
|
||||||
const GeneratedLocalizationsDelegate();
|
|
||||||
|
|
||||||
static S of(BuildContext context) =>
|
static S of(BuildContext context) => Localizations.of<S>(context, WidgetsLocalizations);
|
||||||
Localizations.of<S>(context, WidgetsLocalizations);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextDirection get textDirection => TextDirection.ltr;
|
TextDirection get textDirection => TextDirection.ltr;
|
||||||
|
37
lib/helpers.dart
Normal file
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 {
|
class InvenTreeCompany extends InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreeCompany() : super();
|
||||||
|
|
||||||
|
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get URL => "company/";
|
String get URL => "company/";
|
||||||
|
|
||||||
@ -25,25 +31,51 @@ class InvenTreeCompany extends InvenTreeModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
InvenTreeCompany() : super();
|
String get image => (jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage) as String;
|
||||||
|
|
||||||
String get image => jsondata['image'] ?? jsondata['thumbnail'] ?? InvenTreeAPI.staticImage;
|
String get thumbnail => (jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb) as String;
|
||||||
|
|
||||||
String get thumbnail => jsondata['thumbnail'] ?? jsondata['image'] ?? InvenTreeAPI.staticThumb;
|
String get website => (jsondata["website"] ?? "") as String;
|
||||||
|
|
||||||
String get website => jsondata['website'] ?? '';
|
String get phone => (jsondata["phone"] ?? "") as String;
|
||||||
|
|
||||||
String get phone => jsondata['phone'] ?? '';
|
String get email => (jsondata["email"] ?? "") as String;
|
||||||
|
|
||||||
String get email => jsondata['email'] ?? '';
|
bool get isSupplier => (jsondata["is_supplier"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isSupplier => jsondata['is_supplier'] ?? false;
|
bool get isManufacturer => (jsondata["is_manufacturer"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isManufacturer => jsondata['is_manufacturer'] ?? false;
|
bool get isCustomer => (jsondata["is_customer"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isCustomer => jsondata['is_customer'] ?? false;
|
int get partSuppliedCount => (jsondata["parts_supplied"] ?? 0) as int;
|
||||||
|
|
||||||
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
int get partManufacturedCount => (jsondata["parts_manufactured"] ?? 0) as int;
|
||||||
|
|
||||||
|
// Request a list of purchase orders against this company
|
||||||
|
Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({bool? outstanding}) async {
|
||||||
|
|
||||||
|
Map<String, String> filters = {
|
||||||
|
"supplier": "${pk}"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (outstanding != null) {
|
||||||
|
filters["outstanding"] = outstanding ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<InvenTreeModel> results = await InvenTreePurchaseOrder().list(
|
||||||
|
filters: filters
|
||||||
|
);
|
||||||
|
|
||||||
|
List<InvenTreePurchaseOrder> orders = [];
|
||||||
|
|
||||||
|
for (InvenTreeModel model in results) {
|
||||||
|
if (model is InvenTreePurchaseOrder) {
|
||||||
|
orders.add(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
@ -58,6 +90,11 @@ class InvenTreeCompany extends InvenTreeModel {
|
|||||||
* The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
|
* The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
|
||||||
*/
|
*/
|
||||||
class InvenTreeSupplierPart extends InvenTreeModel {
|
class InvenTreeSupplierPart extends InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreeSupplierPart() : super();
|
||||||
|
|
||||||
|
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get URL => "company/part/";
|
String get URL => "company/part/";
|
||||||
|
|
||||||
@ -79,27 +116,29 @@ class InvenTreeSupplierPart extends InvenTreeModel {
|
|||||||
return _filters();
|
return _filters();
|
||||||
}
|
}
|
||||||
|
|
||||||
InvenTreeSupplierPart() : super();
|
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
|
||||||
|
|
||||||
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
String get manufacturerName => (jsondata["manufacturer_detail"]["name"] ?? "") as String;
|
||||||
|
|
||||||
int get manufacturerId => (jsondata['manufacturer'] ?? -1) as int;
|
String get manufacturerImage => (jsondata["manufacturer_detail"]["image"] ?? jsondata["manufacturer_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||||
|
|
||||||
String get manufacturerName => jsondata['manufacturer_detail']['name'];
|
int get manufacturerPartId => (jsondata["manufacturer_part"] ?? -1) as int;
|
||||||
|
|
||||||
String get manufacturerImage => jsondata['manufacturer_detail']['image'] ?? jsondata['manufacturer_detail']['thumbnail'];
|
int get supplierId => (jsondata["supplier"] ?? -1) as int;
|
||||||
|
|
||||||
int get manufacturerPartId => (jsondata['manufacturer_part'] ?? -1) as int;
|
String get supplierName => (jsondata["supplier_detail"]["name"] ?? "") as String;
|
||||||
|
|
||||||
int get supplierId => (jsondata['supplier'] ?? -1) as int;
|
String get supplierImage => (jsondata["supplier_detail"]["image"] ?? jsondata["supplier_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||||
|
|
||||||
String get supplierName => jsondata['supplier_detail']['name'];
|
String get SKU => (jsondata["SKU"] ?? "") as String;
|
||||||
|
|
||||||
String get supplierImage => jsondata['supplier_detail']['image'] ?? jsondata['supplier_detail']['thumbnail'];
|
String get MPN => (jsondata["MPN"] ?? "") as String;
|
||||||
|
|
||||||
String get SKU => (jsondata['SKU'] ?? '') as String;
|
int get partId => (jsondata["part"] ?? -1) as int;
|
||||||
|
|
||||||
String get MPN => jsondata['MPN'] ?? '';
|
String get partImage => (jsondata["part_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||||
|
|
||||||
|
String get partName => (jsondata["part_detail"]["full_name"] ?? "") as String;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
@ -112,6 +151,10 @@ class InvenTreeSupplierPart extends InvenTreeModel {
|
|||||||
|
|
||||||
class InvenTreeManufacturerPart extends InvenTreeModel {
|
class InvenTreeManufacturerPart extends InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreeManufacturerPart() : super();
|
||||||
|
|
||||||
|
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String url = "company/part/manufacturer/";
|
String url = "company/part/manufacturer/";
|
||||||
|
|
||||||
@ -122,15 +165,11 @@ class InvenTreeManufacturerPart extends InvenTreeModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
InvenTreeManufacturerPart() : super();
|
int get partId => (jsondata["part"] ?? -1) as int;
|
||||||
|
|
||||||
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
|
||||||
|
|
||||||
int get partId => (jsondata['part'] ?? -1) as int;
|
String get MPN => (jsondata["MPN"] ?? "") as String;
|
||||||
|
|
||||||
int get manufacturerId => (jsondata['manufacturer'] ?? -1) as int;
|
|
||||||
|
|
||||||
String get MPN => (jsondata['MPN'] ?? '') as String;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import 'dart:async';
|
import "dart:async";
|
||||||
import 'dart:io';
|
import "dart:io";
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/api.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
import "package:inventree/inventree/sentry.dart";
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
import "package:inventree/widget/dialogs.dart";
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import "package:url_launcher/url_launcher.dart";
|
||||||
|
|
||||||
import 'package:path/path.dart' as path;
|
import "package:path/path.dart" as path;
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
import '../l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
import '../api_form.dart';
|
import "package:inventree/api_form.dart";
|
||||||
|
|
||||||
|
|
||||||
// Paginated response object
|
// Paginated response object
|
||||||
@ -40,12 +39,17 @@ class InvenTreePageResponse {
|
|||||||
*/
|
*/
|
||||||
class InvenTreeModel {
|
class InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreeModel();
|
||||||
|
|
||||||
|
// Construct an InvenTreeModel from a JSON data object
|
||||||
|
InvenTreeModel.fromJson(this.jsondata);
|
||||||
|
|
||||||
// Override the endpoint URL for each subclass
|
// Override the endpoint URL for each subclass
|
||||||
String get URL => "";
|
String get URL => "";
|
||||||
|
|
||||||
// Override the web URL for each subclass
|
// Override the web URL for each subclass
|
||||||
// Note: If the WEB_URL is the same (except for /api/) as URL then just leave blank
|
// Note: If the WEB_URL is the same (except for /api/) as URL then just leave blank
|
||||||
String WEB_URL = "";
|
String get WEB_URL => "";
|
||||||
|
|
||||||
String get webUrl {
|
String get webUrl {
|
||||||
|
|
||||||
@ -114,36 +118,23 @@ class InvenTreeModel {
|
|||||||
Map<String, dynamic> jsondata = {};
|
Map<String, dynamic> jsondata = {};
|
||||||
|
|
||||||
// Accessor for the API
|
// Accessor for the API
|
||||||
var api = InvenTreeAPI();
|
InvenTreeAPI get api => InvenTreeAPI();
|
||||||
|
|
||||||
// Default empty object constructor
|
int get pk => (jsondata["pk"] ?? -1) as int;
|
||||||
InvenTreeModel() {
|
|
||||||
jsondata.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct an InvenTreeModel from a JSON data object
|
|
||||||
InvenTreeModel.fromJson(Map<String, dynamic> json) {
|
|
||||||
|
|
||||||
// Store the json object
|
|
||||||
jsondata = json;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
int get pk => (jsondata['pk'] ?? -1) as int;
|
|
||||||
|
|
||||||
// Some common accessors
|
// Some common accessors
|
||||||
String get name => jsondata['name'] ?? '';
|
String get name => (jsondata["name"] ?? "") as String;
|
||||||
|
|
||||||
String get description => jsondata['description'] ?? '';
|
String get description => (jsondata["description"] ?? "") as String;
|
||||||
|
|
||||||
String get notes => jsondata['notes'] ?? '';
|
String get notes => (jsondata["notes"] ?? "") as String;
|
||||||
|
|
||||||
int get parentId => (jsondata['parent'] ?? -1) as int;
|
int get parentId => (jsondata["parent"] ?? -1) as int;
|
||||||
|
|
||||||
// Legacy API provided external link as "URL", while newer API uses "link"
|
// Legacy API provided external link as "URL", while newer API uses "link"
|
||||||
String get link => jsondata['link'] ?? jsondata['URL'] ?? '';
|
String get link => (jsondata["link"] ?? jsondata["URL"] ?? "") as String;
|
||||||
|
|
||||||
void goToInvenTreePage() async {
|
Future <void> goToInvenTreePage() async {
|
||||||
|
|
||||||
if (await canLaunch(webUrl)) {
|
if (await canLaunch(webUrl)) {
|
||||||
await launch(webUrl);
|
await launch(webUrl);
|
||||||
@ -152,7 +143,7 @@ class InvenTreeModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void openLink() async {
|
Future <void> openLink() async {
|
||||||
|
|
||||||
if (link.isNotEmpty) {
|
if (link.isNotEmpty) {
|
||||||
|
|
||||||
@ -162,7 +153,7 @@ class InvenTreeModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String get keywords => jsondata['keywords'] ?? '';
|
String get keywords => (jsondata["keywords"] ?? "") as String;
|
||||||
|
|
||||||
// Create a new object from JSON data (not a constructor!)
|
// Create a new object from JSON data (not a constructor!)
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
@ -176,20 +167,60 @@ class InvenTreeModel {
|
|||||||
String get url => "${URL}/${pk}/".replaceAll("//", "/");
|
String get url => "${URL}/${pk}/".replaceAll("//", "/");
|
||||||
|
|
||||||
// Search this Model type in the database
|
// Search this Model type in the database
|
||||||
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters = const {}}) async {
|
Future<List<InvenTreeModel>> search(String searchTerm, {Map<String, String> filters = const {}, int offset = 0, int limit = 25}) async {
|
||||||
|
|
||||||
filters["search"] = searchTerm;
|
Map<String, String> searchFilters = {};
|
||||||
|
|
||||||
final results = list(filters: filters);
|
for (String key in filters.keys) {
|
||||||
|
searchFilters[key] = filters[key] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
searchFilters["search"] = searchTerm;
|
||||||
|
searchFilters["offset"] = "${offset}";
|
||||||
|
searchFilters["limit"] = "${limit}";
|
||||||
|
|
||||||
|
final results = list(filters: searchFilters);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> defaultListFilters() { return Map<String, String>(); }
|
// Return the number of results that would meet a particular "query"
|
||||||
|
Future<int> count({Map<String, String> filters = const {}, String searchQuery = ""} ) async {
|
||||||
|
|
||||||
|
var params = defaultListFilters();
|
||||||
|
|
||||||
|
filters.forEach((String key, String value) {
|
||||||
|
params[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchQuery.isNotEmpty) {
|
||||||
|
params["search"] = searchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit to 1 result, for quick DB access
|
||||||
|
params["limit"] = "1";
|
||||||
|
|
||||||
|
var response = await api.get(URL, params: params);
|
||||||
|
|
||||||
|
if (response.isValid()) {
|
||||||
|
int n = int.tryParse(response.data["count"].toString()) ?? 0;
|
||||||
|
|
||||||
|
print("${URL} -> ${n} results");
|
||||||
|
return n;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> defaultListFilters() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
// A map of "default" headers to use when performing a GET request
|
// A map of "default" headers to use when performing a GET request
|
||||||
Map<String, String> defaultGetFilters() { return Map<String, String>(); }
|
Map<String, String> defaultGetFilters() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Reload this object, by requesting data from the server
|
* Reload this object, by requesting data from the server
|
||||||
@ -198,7 +229,7 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
var response = await api.get(url, params: defaultGetFilters(), expectedStatusCode: 200);
|
var response = await api.get(url, params: defaultGetFilters(), expectedStatusCode: 200);
|
||||||
|
|
||||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
if (!response.isValid() || response.data == null || (response.data is! Map)) {
|
||||||
|
|
||||||
// Report error
|
// Report error
|
||||||
if (response.statusCode > 0) {
|
if (response.statusCode > 0) {
|
||||||
@ -224,7 +255,7 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jsondata = response.data;
|
jsondata = response.asMap();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -267,12 +298,12 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
// Override any default values
|
// Override any default values
|
||||||
for (String key in filters.keys) {
|
for (String key in filters.keys) {
|
||||||
params[key] = filters[key] ?? '';
|
params[key] = filters[key] ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await api.get(url, params: params);
|
var response = await api.get(url, params: params);
|
||||||
|
|
||||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
if (!response.isValid() || response.data == null || response.data is! Map) {
|
||||||
|
|
||||||
if (response.statusCode > 0) {
|
if (response.statusCode > 0) {
|
||||||
await sentryReportMessage(
|
await sentryReportMessage(
|
||||||
@ -297,25 +328,23 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createFromJson(response.data);
|
return createFromJson(response.asMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<InvenTreeModel?> create(Map<String, dynamic> data) async {
|
Future<InvenTreeModel?> create(Map<String, dynamic> data) async {
|
||||||
|
|
||||||
print("CREATE: ${URL} ${data.toString()}");
|
if (data.containsKey("pk")) {
|
||||||
|
data.remove("pk");
|
||||||
if (data.containsKey('pk')) {
|
|
||||||
data.remove('pk');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.containsKey('id')) {
|
if (data.containsKey("id")) {
|
||||||
data.remove('id');
|
data.remove("id");
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await api.post(URL, body: data);
|
var response = await api.post(URL, body: data);
|
||||||
|
|
||||||
// Invalid response returned from server
|
// Invalid response returned from server
|
||||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
if (!response.isValid() || response.data == null || response.data is! Map) {
|
||||||
|
|
||||||
if (response.statusCode > 0) {
|
if (response.statusCode > 0) {
|
||||||
await sentryReportMessage(
|
await sentryReportMessage(
|
||||||
@ -340,19 +369,34 @@ class InvenTreeModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createFromJson(response.data);
|
return createFromJson(response.asMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async {
|
Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async {
|
||||||
var params = defaultListFilters();
|
var params = defaultListFilters();
|
||||||
|
|
||||||
for (String key in filters.keys) {
|
for (String key in filters.keys) {
|
||||||
params[key] = filters[key] ?? '';
|
params[key] = filters[key] ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
params["limit"] = "${limit}";
|
params["limit"] = "${limit}";
|
||||||
params["offset"] = "${offset}";
|
params["offset"] = "${offset}";
|
||||||
|
|
||||||
|
/* Special case: "original_search":
|
||||||
|
* - We may wish to provide an original "query" which is augmented by the user
|
||||||
|
* - Thus, "search" and "original_search" may both be provided
|
||||||
|
* - In such a case, we want to concatenate them together
|
||||||
|
*/
|
||||||
|
if (params.containsKey("original_search")) {
|
||||||
|
|
||||||
|
String search = params["search"] ?? "";
|
||||||
|
String original = params["original_search"] ?? "";
|
||||||
|
|
||||||
|
params["search"] = "${search} ${original}".trim();
|
||||||
|
|
||||||
|
params.remove("original_search");
|
||||||
|
}
|
||||||
|
|
||||||
var response = await api.get(URL, params: params);
|
var response = await api.get(URL, params: params);
|
||||||
|
|
||||||
if (!response.isValid()) {
|
if (!response.isValid()) {
|
||||||
@ -360,15 +404,17 @@ class InvenTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct the response
|
// Construct the response
|
||||||
InvenTreePageResponse page = new InvenTreePageResponse();
|
InvenTreePageResponse page = InvenTreePageResponse();
|
||||||
|
|
||||||
if (response.data.containsKey("count") && response.data.containsKey("results")) {
|
var data = response.asMap();
|
||||||
page.count = response.data["count"] as int;
|
|
||||||
|
if (data.containsKey("count") && data.containsKey("results")) {
|
||||||
|
page.count = (data["count"] ?? 0) as int;
|
||||||
|
|
||||||
page.results = [];
|
page.results = [];
|
||||||
|
|
||||||
for (var result in response.data["results"]) {
|
for (var result in response.data["results"]) {
|
||||||
page.addResult(createFromJson(result));
|
page.addResult(createFromJson(result as Map<String, dynamic>));
|
||||||
}
|
}
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
@ -384,7 +430,7 @@ class InvenTreeModel {
|
|||||||
var params = defaultListFilters();
|
var params = defaultListFilters();
|
||||||
|
|
||||||
for (String key in filters.keys) {
|
for (String key in filters.keys) {
|
||||||
params[key] = filters[key] ?? '';
|
params[key] = filters[key] ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await api.get(URL, params: params);
|
var response = await api.get(URL, params: params);
|
||||||
@ -396,20 +442,22 @@ class InvenTreeModel {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic data;
|
List<dynamic> data = [];
|
||||||
|
|
||||||
if (response.data is List) {
|
if (response.isList()) {
|
||||||
data = response.data;
|
data = response.asList();
|
||||||
} else if (response.data.containsKey('results')) {
|
} else if (response.isMap()) {
|
||||||
data = response.data['results'];
|
var mData = response.asMap();
|
||||||
} else {
|
|
||||||
data = [];
|
if (mData.containsKey("results")) {
|
||||||
|
data = (response.data["results"] ?? []) as List<dynamic>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var d in data) {
|
for (var d in data) {
|
||||||
|
|
||||||
// Create a new object (of the current class type
|
// Create a new object (of the current class type
|
||||||
InvenTreeModel obj = createFromJson(d);
|
InvenTreeModel obj = createFromJson(d as Map<String, dynamic>);
|
||||||
|
|
||||||
results.add(obj);
|
results.add(obj);
|
||||||
}
|
}
|
||||||
@ -421,9 +469,9 @@ class InvenTreeModel {
|
|||||||
// Provide a listing of objects at the endpoint
|
// Provide a listing of objects at the endpoint
|
||||||
// TODO - Static function which returns a list of objects (of this class)
|
// TODO - Static function which returns a list of objects (of this class)
|
||||||
|
|
||||||
// TODO - Define a 'delete' function
|
// TODO - Define a "delete" function
|
||||||
|
|
||||||
// TODO - Define a 'save' / 'update' function
|
// TODO - Define a "save" / "update" function
|
||||||
|
|
||||||
// Override this function for each sub-class
|
// Override this function for each sub-class
|
||||||
bool matchAgainstString(String filter) {
|
bool matchAgainstString(String filter) {
|
||||||
@ -457,10 +505,11 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
class InvenTreeAttachment extends InvenTreeModel {
|
class InvenTreeAttachment extends InvenTreeModel {
|
||||||
// Class representing an "attachment" file
|
// Class representing an "attachment" file
|
||||||
|
|
||||||
InvenTreeAttachment() : super();
|
InvenTreeAttachment() : super();
|
||||||
|
|
||||||
String get attachment => jsondata["attachment"] ?? '';
|
InvenTreeAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
|
String get attachment => (jsondata["attachment"] ?? "") as String;
|
||||||
|
|
||||||
// Return the filename of the attachment
|
// Return the filename of the attachment
|
||||||
String get filename {
|
String get filename {
|
||||||
@ -498,25 +547,23 @@ class InvenTreeAttachment extends InvenTreeModel {
|
|||||||
return FontAwesomeIcons.fileAlt;
|
return FontAwesomeIcons.fileAlt;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get comment => jsondata["comment"] ?? '';
|
String get comment => (jsondata["comment"] ?? "") as String;
|
||||||
|
|
||||||
DateTime? get uploadDate {
|
DateTime? get uploadDate {
|
||||||
if (jsondata.containsKey("upload_date")) {
|
if (jsondata.containsKey("upload_date")) {
|
||||||
return DateTime.tryParse(jsondata["upload_date"] ?? '');
|
return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InvenTreeAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
|
||||||
|
|
||||||
Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async {
|
Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async {
|
||||||
|
|
||||||
final APIResponse response = await InvenTreeAPI().uploadFile(
|
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||||
URL,
|
URL,
|
||||||
attachment,
|
attachment,
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
name: 'attachment',
|
name: "attachment",
|
||||||
fields: fields
|
fields: fields
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import 'package:inventree/api.dart';
|
import "dart:io";
|
||||||
import 'package:inventree/inventree/stock.dart';
|
|
||||||
import 'package:inventree/inventree/company.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:inventree/l10.dart';
|
|
||||||
|
|
||||||
import 'model.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'dart:io';
|
import "package:inventree/inventree/stock.dart";
|
||||||
import 'package:http/http.dart' as http;
|
import "package:inventree/inventree/company.dart";
|
||||||
|
import "package:flutter/cupertino.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
|
import "model.dart";
|
||||||
|
|
||||||
class InvenTreePartCategory extends InvenTreeModel {
|
class InvenTreePartCategory extends InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreePartCategory() : super();
|
||||||
|
|
||||||
|
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get URL => "part/category/";
|
String get URL => "part/category/";
|
||||||
|
|
||||||
@ -25,21 +29,20 @@ class InvenTreePartCategory extends InvenTreeModel {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, String> defaultListFilters() {
|
Map<String, String> defaultListFilters() {
|
||||||
var filters = new Map<String, String>();
|
|
||||||
|
|
||||||
filters["active"] = "true";
|
return {
|
||||||
filters["cascade"] = "false";
|
"active": "true",
|
||||||
|
"cascade": "false"
|
||||||
return filters;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
String get pathstring => jsondata['pathstring'] ?? '';
|
String get pathstring => (jsondata["pathstring"] ?? "") as String;
|
||||||
|
|
||||||
String get parentpathstring {
|
String get parentpathstring {
|
||||||
// TODO - Drive the refactor tractor through this
|
// TODO - Drive the refactor tractor through this
|
||||||
List<String> psplit = pathstring.split("/");
|
List<String> psplit = pathstring.split("/");
|
||||||
|
|
||||||
if (psplit.length > 0) {
|
if (psplit.isNotEmpty) {
|
||||||
psplit.removeLast();
|
psplit.removeLast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,11 +55,7 @@ class InvenTreePartCategory extends InvenTreeModel {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get partcount => jsondata['parts'] ?? 0;
|
int get partcount => (jsondata["parts"] ?? 0) as int;
|
||||||
|
|
||||||
InvenTreePartCategory() : super();
|
|
||||||
|
|
||||||
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
@ -71,25 +70,23 @@ class InvenTreePartCategory extends InvenTreeModel {
|
|||||||
|
|
||||||
class InvenTreePartTestTemplate extends InvenTreeModel {
|
class InvenTreePartTestTemplate extends InvenTreeModel {
|
||||||
|
|
||||||
@override
|
|
||||||
String get URL => "part/test-template/";
|
|
||||||
|
|
||||||
String get key => jsondata['key'] ?? '';
|
|
||||||
|
|
||||||
String get testName => jsondata['test_name'] ?? '';
|
|
||||||
|
|
||||||
String get description => jsondata['description'] ?? '';
|
|
||||||
|
|
||||||
bool get required => jsondata['required'] ?? false;
|
|
||||||
|
|
||||||
bool get requiresValue => jsondata['requires_value'] ?? false;
|
|
||||||
|
|
||||||
bool get requiresAttachment => jsondata['requires_attachment'] ?? false;
|
|
||||||
|
|
||||||
InvenTreePartTestTemplate() : super();
|
InvenTreePartTestTemplate() : super();
|
||||||
|
|
||||||
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get URL => "part/test-template/";
|
||||||
|
|
||||||
|
String get key => (jsondata["key"] ?? "") as String;
|
||||||
|
|
||||||
|
String get testName => (jsondata["test_name"] ?? "") as String;
|
||||||
|
|
||||||
|
bool get required => (jsondata["required"] ?? false) as bool;
|
||||||
|
|
||||||
|
bool get requiresValue => (jsondata["requires_value"] ?? false) as bool;
|
||||||
|
|
||||||
|
bool get requiresAttachment => (jsondata["requires_attachment"] ?? false) as bool;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
var template = InvenTreePartTestTemplate.fromJson(json);
|
var template = InvenTreePartTestTemplate.fromJson(json);
|
||||||
@ -125,6 +122,10 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
|
|||||||
|
|
||||||
class InvenTreePart extends InvenTreeModel {
|
class InvenTreePart extends InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreePart() : super();
|
||||||
|
|
||||||
|
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get URL => "part/";
|
String get URL => "part/";
|
||||||
|
|
||||||
@ -138,9 +139,9 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
"keywords": {},
|
"keywords": {},
|
||||||
"link": {},
|
"link": {},
|
||||||
|
|
||||||
// Parent category
|
"category": {},
|
||||||
"category": {
|
|
||||||
},
|
"default_location": {},
|
||||||
|
|
||||||
"units": {},
|
"units": {},
|
||||||
|
|
||||||
@ -195,7 +196,7 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
int get supplierCount => (jsondata['suppliers'] ?? 0) as int;
|
int get supplierCount => (jsondata["suppliers"] ?? 0) as int;
|
||||||
|
|
||||||
// Request supplier parts for this part
|
// Request supplier parts for this part
|
||||||
Future<List<InvenTreeSupplierPart>> getSupplierParts() async {
|
Future<List<InvenTreeSupplierPart>> getSupplierParts() async {
|
||||||
@ -241,8 +242,10 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? get defaultLocation => jsondata["default_location"] as int?;
|
||||||
|
|
||||||
// Get the number of stock on order for this Part
|
// Get the number of stock on order for this Part
|
||||||
double get onOrder => double.tryParse(jsondata['ordering'].toString()) ?? 0;
|
double get onOrder => double.tryParse(jsondata["ordering"].toString()) ?? 0;
|
||||||
|
|
||||||
String get onOrderString {
|
String get onOrderString {
|
||||||
|
|
||||||
@ -254,7 +257,7 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the stock count for this Part
|
// Get the stock count for this Part
|
||||||
double get inStock => double.tryParse(jsondata['in_stock'].toString()) ?? 0;
|
double get inStock => double.tryParse(jsondata["in_stock"].toString()) ?? 0;
|
||||||
|
|
||||||
String get inStockString {
|
String get inStockString {
|
||||||
|
|
||||||
@ -271,69 +274,69 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get units => jsondata["units"] ?? "";
|
String get units => (jsondata["units"] ?? "") as String;
|
||||||
|
|
||||||
// Get the number of units being build for this Part
|
// Get the number of units being build for this Part
|
||||||
double get building => double.tryParse(jsondata['building'].toString()) ?? 0;
|
double get building => double.tryParse(jsondata["building"].toString()) ?? 0;
|
||||||
|
|
||||||
// Get the number of BOM items in this Part (if it is an assembly)
|
// Get the number of BOM items in this Part (if it is an assembly)
|
||||||
int get bomItemCount => (jsondata['bom_items'] ?? 0) as int;
|
int get bomItemCount => (jsondata["bom_items"] ?? 0) as int;
|
||||||
|
|
||||||
// Get the number of BOMs this Part is used in (if it is a component)
|
// Get the number of BOMs this Part is used in (if it is a component)
|
||||||
int get usedInCount => (jsondata['used_in'] ?? 0) as int;
|
int get usedInCount => (jsondata["used_in"] ?? 0) as int;
|
||||||
|
|
||||||
bool get isAssembly => (jsondata['assembly'] ?? false) as bool;
|
bool get isAssembly => (jsondata["assembly"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isComponent => (jsondata['component'] ?? false) as bool;
|
bool get isComponent => (jsondata["component"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isPurchaseable => (jsondata['purchaseable'] ?? false) as bool;
|
bool get isPurchaseable => (jsondata["purchaseable"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isSalable => (jsondata['salable'] ?? false) as bool;
|
bool get isSalable => (jsondata["salable"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isActive => (jsondata['active'] ?? false) as bool;
|
bool get isActive => (jsondata["active"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isVirtual => (jsondata['virtual'] ?? false) as bool;
|
bool get isVirtual => (jsondata["virtual"] ?? false) as bool;
|
||||||
|
|
||||||
bool get isTrackable => (jsondata['trackable'] ?? false) as bool;
|
bool get isTrackable => (jsondata["trackable"] ?? false) as bool;
|
||||||
|
|
||||||
// Get the IPN (internal part number) for the Part instance
|
// Get the IPN (internal part number) for the Part instance
|
||||||
String get IPN => jsondata['IPN'] ?? '';
|
String get IPN => (jsondata["IPN"] ?? "") as String;
|
||||||
|
|
||||||
// Get the revision string for the Part instance
|
// Get the revision string for the Part instance
|
||||||
String get revision => jsondata['revision'] ?? '';
|
String get revision => (jsondata["revision"] ?? "") as String;
|
||||||
|
|
||||||
// Get the category ID for the Part instance (or 'null' if does not exist)
|
// Get the category ID for the Part instance (or "null" if does not exist)
|
||||||
int get categoryId => (jsondata['category'] ?? -1) as int;
|
int get categoryId => (jsondata["category"] ?? -1) as int;
|
||||||
|
|
||||||
// Get the category name for the Part instance
|
// Get the category name for the Part instance
|
||||||
String get categoryName {
|
String get categoryName {
|
||||||
// Inavlid category ID
|
// Inavlid category ID
|
||||||
if (categoryId <= 0) return '';
|
if (categoryId <= 0) return "";
|
||||||
|
|
||||||
if (!jsondata.containsKey('category_detail')) return '';
|
if (!jsondata.containsKey("category_detail")) return "";
|
||||||
|
|
||||||
return jsondata['category_detail']?['name'] ?? '';
|
return (jsondata["category_detail"]?["name"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the category description for the Part instance
|
// Get the category description for the Part instance
|
||||||
String get categoryDescription {
|
String get categoryDescription {
|
||||||
// Invalid category ID
|
// Invalid category ID
|
||||||
if (categoryId <= 0) return '';
|
if (categoryId <= 0) return "";
|
||||||
|
|
||||||
if (!jsondata.containsKey('category_detail')) return '';
|
if (!jsondata.containsKey("category_detail")) return "";
|
||||||
|
|
||||||
return jsondata['category_detail']?['description'] ?? '';
|
return (jsondata["category_detail"]?["description"] ?? "") as String;
|
||||||
}
|
}
|
||||||
// Get the image URL for the Part instance
|
// Get the image URL for the Part instance
|
||||||
String get _image => jsondata['image'] ?? '';
|
String get _image => (jsondata["image"] ?? "") as String;
|
||||||
|
|
||||||
// Get the thumbnail URL for the Part instance
|
// Get the thumbnail URL for the Part instance
|
||||||
String get _thumbnail => jsondata['thumbnail'] ?? '';
|
String get _thumbnail => (jsondata["thumbnail"] ?? "") as String;
|
||||||
|
|
||||||
// Return the fully-qualified name for the Part instance
|
// Return the fully-qualified name for the Part instance
|
||||||
String get fullname {
|
String get fullname {
|
||||||
|
|
||||||
String fn = jsondata['full_name'] ?? '';
|
String fn = (jsondata["full_name"] ?? "") as String;
|
||||||
|
|
||||||
if (fn.isNotEmpty) return fn;
|
if (fn.isNotEmpty) return fn;
|
||||||
|
|
||||||
@ -369,21 +372,15 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
final APIResponse response = await InvenTreeAPI().uploadFile(
|
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||||
url,
|
url,
|
||||||
image,
|
image,
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
name: 'image',
|
name: "image",
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.successful();
|
return response.successful();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the "starred" status of this part
|
// Return the "starred" status of this part
|
||||||
bool get starred => (jsondata['starred'] ?? false) as bool;
|
bool get starred => (jsondata["starred"] ?? false) as bool;
|
||||||
|
|
||||||
InvenTreePart() : super();
|
|
||||||
|
|
||||||
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
@ -399,11 +396,11 @@ class InvenTreePartAttachment extends InvenTreeAttachment {
|
|||||||
|
|
||||||
InvenTreePartAttachment() : super();
|
InvenTreePartAttachment() : super();
|
||||||
|
|
||||||
|
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get URL => "part/attachment/";
|
String get URL => "part/attachment/";
|
||||||
|
|
||||||
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
return InvenTreePartAttachment.fromJson(json);
|
return InvenTreePartAttachment.fromJson(json);
|
||||||
|
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:device_info_plus/device_info_plus.dart";
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import "package:package_info_plus/package_info_plus.dart";
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
import "package:sentry_flutter/sentry_flutter.dart";
|
||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import "package:inventree/api.dart";
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getDeviceInfo() async {
|
Future<Map<String, dynamic>> getDeviceInfo() async {
|
||||||
|
|
||||||
@ -18,35 +18,35 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
|
|||||||
final iosDeviceInfo = await deviceInfo.iosInfo;
|
final iosDeviceInfo = await deviceInfo.iosInfo;
|
||||||
|
|
||||||
device_info = {
|
device_info = {
|
||||||
'name': iosDeviceInfo.name,
|
"name": iosDeviceInfo.name,
|
||||||
'model': iosDeviceInfo.model,
|
"model": iosDeviceInfo.model,
|
||||||
'systemName': iosDeviceInfo.systemName,
|
"systemName": iosDeviceInfo.systemName,
|
||||||
'systemVersion': iosDeviceInfo.systemVersion,
|
"systemVersion": iosDeviceInfo.systemVersion,
|
||||||
'localizedModel': iosDeviceInfo.localizedModel,
|
"localizedModel": iosDeviceInfo.localizedModel,
|
||||||
'utsname': iosDeviceInfo.utsname.sysname,
|
"utsname": iosDeviceInfo.utsname.sysname,
|
||||||
'identifierForVendor': iosDeviceInfo.identifierForVendor,
|
"identifierForVendor": iosDeviceInfo.identifierForVendor,
|
||||||
'isPhysicalDevice': iosDeviceInfo.isPhysicalDevice,
|
"isPhysicalDevice": iosDeviceInfo.isPhysicalDevice,
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (Platform.isAndroid) {
|
} else if (Platform.isAndroid) {
|
||||||
final androidDeviceInfo = await deviceInfo.androidInfo;
|
final androidDeviceInfo = await deviceInfo.androidInfo;
|
||||||
|
|
||||||
device_info = {
|
device_info = {
|
||||||
'type': androidDeviceInfo.type,
|
"type": androidDeviceInfo.type,
|
||||||
'model': androidDeviceInfo.model,
|
"model": androidDeviceInfo.model,
|
||||||
'device': androidDeviceInfo.device,
|
"device": androidDeviceInfo.device,
|
||||||
'id': androidDeviceInfo.id,
|
"id": androidDeviceInfo.id,
|
||||||
'androidId': androidDeviceInfo.androidId,
|
"androidId": androidDeviceInfo.androidId,
|
||||||
'brand': androidDeviceInfo.brand,
|
"brand": androidDeviceInfo.brand,
|
||||||
'display': androidDeviceInfo.display,
|
"display": androidDeviceInfo.display,
|
||||||
'hardware': androidDeviceInfo.hardware,
|
"hardware": androidDeviceInfo.hardware,
|
||||||
'manufacturer': androidDeviceInfo.manufacturer,
|
"manufacturer": androidDeviceInfo.manufacturer,
|
||||||
'product': androidDeviceInfo.product,
|
"product": androidDeviceInfo.product,
|
||||||
'version': androidDeviceInfo.version.release,
|
"version": androidDeviceInfo.version.release,
|
||||||
'supported32BitAbis': androidDeviceInfo.supported32BitAbis,
|
"supported32BitAbis": androidDeviceInfo.supported32BitAbis,
|
||||||
'supported64BitAbis': androidDeviceInfo.supported64BitAbis,
|
"supported64BitAbis": androidDeviceInfo.supported64BitAbis,
|
||||||
'supportedAbis': androidDeviceInfo.supportedAbis,
|
"supportedAbis": androidDeviceInfo.supportedAbis,
|
||||||
'isPhysicalDevice': androidDeviceInfo.isPhysicalDevice,
|
"isPhysicalDevice": androidDeviceInfo.isPhysicalDevice,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
|
|||||||
|
|
||||||
if (isInDebugMode()) {
|
if (isInDebugMode()) {
|
||||||
|
|
||||||
print('----- In dev mode. Not sending message to Sentry.io -----');
|
print("----- In dev mode. Not sending message to Sentry.io -----");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
|
|||||||
|
|
||||||
Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
|
Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
|
||||||
|
|
||||||
print('----- Sentry Intercepted error: $error -----');
|
print("----- Sentry Intercepted error: $error -----");
|
||||||
print(stackTrace);
|
print(stackTrace);
|
||||||
|
|
||||||
// Errors thrown in development mode are unlikely to be interesting. You can
|
// Errors thrown in development mode are unlikely to be interesting. You can
|
||||||
@ -125,7 +125,7 @@ Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
|
|||||||
// the report.
|
// the report.
|
||||||
if (isInDebugMode()) {
|
if (isInDebugMode()) {
|
||||||
|
|
||||||
print('----- In dev mode. Not sending report to Sentry.io -----');
|
print("----- In dev mode. Not sending report to Sentry.io -----");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
import 'package:intl/intl.dart';
|
import "dart:async";
|
||||||
import 'package:inventree/inventree/part.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'model.dart';
|
|
||||||
import 'package:inventree/l10.dart';
|
|
||||||
|
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:intl/intl.dart";
|
||||||
|
import "package:inventree/helpers.dart";
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:flutter/cupertino.dart";
|
||||||
|
|
||||||
import 'dart:async';
|
import "package:inventree/inventree/model.dart";
|
||||||
import 'dart:io';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import "package:inventree/api.dart";
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeStockItemTestResult extends InvenTreeModel {
|
class InvenTreeStockItemTestResult extends InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreeStockItemTestResult() : super();
|
||||||
|
|
||||||
|
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get URL => "stock/test/";
|
String get URL => "stock/test/";
|
||||||
|
|
||||||
@ -31,23 +35,17 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
String get key => jsondata['key'] ?? '';
|
String get key => (jsondata["key"] ?? "") as String;
|
||||||
|
|
||||||
String get testName => jsondata['test'] ?? '';
|
String get testName => (jsondata["test"] ?? "") as String;
|
||||||
|
|
||||||
bool get result => jsondata['result'] ?? false;
|
bool get result => (jsondata["result"] ?? false) as bool;
|
||||||
|
|
||||||
String get value => jsondata['value'] ?? '';
|
String get value => (jsondata["value"] ?? "") as String;
|
||||||
|
|
||||||
String get notes => jsondata['notes'] ?? '';
|
String get attachment => (jsondata["attachment"] ?? "") as String;
|
||||||
|
|
||||||
String get attachment => jsondata['attachment'] ?? '';
|
String get date => (jsondata["date"] ?? "") as String;
|
||||||
|
|
||||||
String get date => jsondata['date'] ?? '';
|
|
||||||
|
|
||||||
InvenTreeStockItemTestResult() : super();
|
|
||||||
|
|
||||||
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
|
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
|
||||||
@ -60,6 +58,10 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
|
|||||||
|
|
||||||
class InvenTreeStockItem extends InvenTreeModel {
|
class InvenTreeStockItem extends InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreeStockItem() : super();
|
||||||
|
|
||||||
|
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
// Stock status codes
|
// Stock status codes
|
||||||
static const int OK = 10;
|
static const int OK = 10;
|
||||||
static const int ATTENTION = 50;
|
static const int ATTENTION = 50;
|
||||||
@ -97,7 +99,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
Color get statusColor {
|
Color get statusColor {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case OK:
|
case OK:
|
||||||
return Color(0xFF50aa51);
|
return Colors.black;
|
||||||
case ATTENTION:
|
case ATTENTION:
|
||||||
return Color(0xFFfdc82a);
|
return Color(0xFFfdc82a);
|
||||||
case DAMAGED:
|
case DAMAGED:
|
||||||
@ -114,7 +116,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
String get URL => "stock/";
|
String get URL => "stock/";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String WEB_URL = "stock/item/";
|
String get WEB_URL => "stock/item/";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> formFields() {
|
Map<String, dynamic> formFields() {
|
||||||
@ -132,33 +134,24 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
@override
|
@override
|
||||||
Map<String, String> defaultGetFilters() {
|
Map<String, String> defaultGetFilters() {
|
||||||
|
|
||||||
var headers = new Map<String, String>();
|
return {
|
||||||
|
"part_detail": "true",
|
||||||
headers["part_detail"] = "true";
|
"location_detail": "true",
|
||||||
headers["location_detail"] = "true";
|
"supplier_detail": "true",
|
||||||
headers["supplier_detail"] = "true";
|
"cascade": "false"
|
||||||
headers["cascade"] = "false";
|
};
|
||||||
|
|
||||||
return headers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, String> defaultListFilters() {
|
Map<String, String> defaultListFilters() {
|
||||||
|
|
||||||
var headers = new Map<String, String>();
|
return {
|
||||||
|
"part_detail": "true",
|
||||||
headers["part_detail"] = "true";
|
"location_detail": "true",
|
||||||
headers["location_detail"] = "true";
|
"supplier_detail": "true",
|
||||||
headers["supplier_detail"] = "true";
|
"cascade": "false",
|
||||||
headers["cascade"] = "false";
|
"in_stock": "true",
|
||||||
|
};
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
InvenTreeStockItem() : super();
|
|
||||||
|
|
||||||
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
|
||||||
// TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<InvenTreePartTestTemplate> testTemplates = [];
|
List<InvenTreePartTestTemplate> testTemplates = [];
|
||||||
@ -204,17 +197,17 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String get uid => jsondata['uid'] ?? '';
|
String get uid => (jsondata["uid"] ?? "") as String;
|
||||||
|
|
||||||
int get status => jsondata['status'] ?? -1;
|
int get status => (jsondata["status"] ?? -1) as int;
|
||||||
|
|
||||||
String get packaging => jsondata["packaging"] ?? "";
|
String get packaging => (jsondata["packaging"] ?? "") as String;
|
||||||
|
|
||||||
String get batch => jsondata["batch"] ?? "";
|
String get batch => (jsondata["batch"] ?? "") as String;
|
||||||
|
|
||||||
int get partId => jsondata['part'] ?? -1;
|
int get partId => (jsondata["part"] ?? -1) as int;
|
||||||
|
|
||||||
String get purchasePrice => jsondata['purchase_price'] ?? "";
|
String get purchasePrice => (jsondata["purchase_price"] ?? "") as String;
|
||||||
|
|
||||||
bool get hasPurchasePrice {
|
bool get hasPurchasePrice {
|
||||||
|
|
||||||
@ -223,12 +216,14 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
return pp.isNotEmpty && pp.trim() != "-";
|
return pp.isNotEmpty && pp.trim() != "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
|
int get purchaseOrderId => (jsondata["purchase_order"] ?? -1) as int;
|
||||||
|
|
||||||
|
int get trackingItemCount => (jsondata["tracking_items"] ?? 0) as int;
|
||||||
|
|
||||||
// Date of last update
|
// Date of last update
|
||||||
DateTime? get updatedDate {
|
DateTime? get updatedDate {
|
||||||
if (jsondata.containsKey("updated")) {
|
if (jsondata.containsKey("updated")) {
|
||||||
return DateTime.tryParse(jsondata["updated"] ?? '');
|
return DateTime.tryParse((jsondata["updated"] ?? "") as String);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -248,7 +243,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
DateTime? get stocktakeDate {
|
DateTime? get stocktakeDate {
|
||||||
if (jsondata.containsKey("stocktake_date")) {
|
if (jsondata.containsKey("stocktake_date")) {
|
||||||
return DateTime.tryParse(jsondata["stocktake_date"] ?? '');
|
return DateTime.tryParse((jsondata["stocktake_date"] ?? "") as String);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -268,45 +263,45 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
String get partName {
|
String get partName {
|
||||||
|
|
||||||
String nm = '';
|
String nm = "";
|
||||||
|
|
||||||
// Use the detailed part information as priority
|
// Use the detailed part information as priority
|
||||||
if (jsondata.containsKey('part_detail')) {
|
if (jsondata.containsKey("part_detail")) {
|
||||||
nm = jsondata['part_detail']['full_name'] ?? '';
|
nm = (jsondata["part_detail"]["full_name"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup if first value fails
|
// Backup if first value fails
|
||||||
if (nm.isEmpty) {
|
if (nm.isEmpty) {
|
||||||
nm = jsondata['part__name'] ?? '';
|
nm = (jsondata["part__name"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nm;
|
return nm;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get partDescription {
|
String get partDescription {
|
||||||
String desc = '';
|
String desc = "";
|
||||||
|
|
||||||
// Use the detailed part description as priority
|
// Use the detailed part description as priority
|
||||||
if (jsondata.containsKey('part_detail')) {
|
if (jsondata.containsKey("part_detail")) {
|
||||||
desc = jsondata['part_detail']['description'] ?? '';
|
desc = (jsondata["part_detail"]["description"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (desc.isEmpty) {
|
if (desc.isEmpty) {
|
||||||
desc = jsondata['part__description'] ?? '';
|
desc = (jsondata["part__description"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
return desc;
|
return desc;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get partImage {
|
String get partImage {
|
||||||
String img = '';
|
String img = "";
|
||||||
|
|
||||||
if (jsondata.containsKey('part_detail')) {
|
if (jsondata.containsKey("part_detail")) {
|
||||||
img = jsondata['part_detail']['thumbnail'] ?? '';
|
img = (jsondata["part_detail"]["thumbnail"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (img.isEmpty) {
|
if (img.isEmpty) {
|
||||||
img = jsondata['part__thumbnail'] ?? '';
|
img = (jsondata["part__thumbnail"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
return img;
|
return img;
|
||||||
@ -319,107 +314,97 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
String thumb = "";
|
String thumb = "";
|
||||||
|
|
||||||
thumb = jsondata['part_detail']?['thumbnail'] ?? '';
|
thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
|
||||||
|
|
||||||
// Use 'image' as a backup
|
// Use "image" as a backup
|
||||||
if (thumb.isEmpty) {
|
if (thumb.isEmpty) {
|
||||||
thumb = jsondata['part_detail']?['image'] ?? '';
|
thumb = (jsondata["part_detail"]?["image"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try a different approach
|
// Try a different approach
|
||||||
if (thumb.isEmpty) {
|
if (thumb.isEmpty) {
|
||||||
thumb = jsondata['part__thumbnail'] ?? '';
|
thumb = (jsondata["part__thumbnail"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Still no thumbnail? Use the 'no image' image
|
// Still no thumbnail? Use the "no image" image
|
||||||
if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb;
|
if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb;
|
||||||
|
|
||||||
return thumb;
|
return thumb;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get supplierPartId => (jsondata['supplier_part'] ?? -1) as int;
|
int get supplierPartId => (jsondata["supplier_part"] ?? -1) as int;
|
||||||
|
|
||||||
String get supplierImage {
|
String get supplierImage {
|
||||||
String thumb = '';
|
String thumb = "";
|
||||||
|
|
||||||
if (jsondata.containsKey("supplier_detail")) {
|
if (jsondata.containsKey("supplier_detail")) {
|
||||||
thumb = jsondata['supplier_detail']['supplier_logo'] ?? '';
|
thumb = (jsondata["supplier_detail"]["supplier_logo"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
return thumb;
|
return thumb;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get supplierName {
|
String get supplierName {
|
||||||
String sname = '';
|
String sname = "";
|
||||||
|
|
||||||
if (jsondata.containsKey("supplier_detail")) {
|
if (jsondata.containsKey("supplier_detail")) {
|
||||||
sname = jsondata["supplier_detail"]["supplier_name"] ?? '';
|
sname = (jsondata["supplier_detail"]["supplier_name"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sname;
|
return sname;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get units {
|
String get units {
|
||||||
return jsondata['part_detail']?['units'] ?? '';
|
return (jsondata["part_detail"]?["units"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get supplierSKU {
|
String get supplierSKU {
|
||||||
String sku = '';
|
String sku = "";
|
||||||
|
|
||||||
if (jsondata.containsKey("supplier_detail")) {
|
if (jsondata.containsKey("supplier_detail")) {
|
||||||
sku = jsondata["supplier_detail"]["SKU"] ?? '';
|
sku = (jsondata["supplier_detail"]["SKU"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sku;
|
return sku;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get serialNumber => jsondata['serial'] ?? "";
|
String get serialNumber => (jsondata["serial"] ?? "") as String;
|
||||||
|
|
||||||
double get quantity => double.tryParse(jsondata['quantity'].toString()) ?? 0;
|
double get quantity => double.tryParse(jsondata["quantity"].toString()) ?? 0;
|
||||||
|
|
||||||
String get quantityString {
|
String quantityString({bool includeUnits = false}){
|
||||||
|
|
||||||
String q = quantity.toString();
|
String q = simpleNumberString(quantity);
|
||||||
|
|
||||||
// Simplify integer values e.g. "1.0" becomes "1"
|
if (includeUnits && units.isNotEmpty) {
|
||||||
if (quantity.toInt() == quantity) {
|
|
||||||
q = quantity.toInt().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (units.isNotEmpty) {
|
|
||||||
q += " ${units}";
|
q += " ${units}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get locationId => (jsondata['location'] ?? -1) as int;
|
int get locationId => (jsondata["location"] ?? -1) as int;
|
||||||
|
|
||||||
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
|
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
|
||||||
|
|
||||||
String serialOrQuantityDisplay() {
|
String serialOrQuantityDisplay() {
|
||||||
if (isSerialized()) {
|
if (isSerialized()) {
|
||||||
return 'SN ${serialNumber}';
|
return "SN ${serialNumber}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is an integer?
|
return simpleNumberString(quantity);
|
||||||
if (quantity.toInt() == quantity) {
|
|
||||||
return '${quantity.toInt()}';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '${quantity}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String get locationName {
|
String get locationName {
|
||||||
String loc = '';
|
String loc = "";
|
||||||
|
|
||||||
if (locationId == -1 || !jsondata.containsKey('location_detail')) return 'Unknown Location';
|
if (locationId == -1 || !jsondata.containsKey("location_detail")) return "Unknown Location";
|
||||||
|
|
||||||
loc = jsondata['location_detail']['name'] ?? '';
|
loc = (jsondata["location_detail"]["name"] ?? "") as String;
|
||||||
|
|
||||||
// Old-style name
|
// Old-style name
|
||||||
if (loc.isEmpty) {
|
if (loc.isEmpty) {
|
||||||
loc = jsondata['location__name'] ?? '';
|
loc = (jsondata["location__name"] ?? "") as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
return loc;
|
return loc;
|
||||||
@ -427,9 +412,9 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
String get locationPathString {
|
String get locationPathString {
|
||||||
|
|
||||||
if (locationId == -1 || !jsondata.containsKey('location_detail')) return L10().locationNotSet;
|
if (locationId == -1 || !jsondata.containsKey("location_detail")) return L10().locationNotSet;
|
||||||
|
|
||||||
String _loc = jsondata['location_detail']['pathstring'] ?? '';
|
String _loc = (jsondata["location_detail"]["pathstring"] ?? "") as String;
|
||||||
|
|
||||||
if (_loc.isNotEmpty) {
|
if (_loc.isNotEmpty) {
|
||||||
return _loc;
|
return _loc;
|
||||||
@ -444,7 +429,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
if (serialNumber.isNotEmpty) {
|
if (serialNumber.isNotEmpty) {
|
||||||
return "SN: $serialNumber";
|
return "SN: $serialNumber";
|
||||||
} else {
|
} else {
|
||||||
return quantityString;
|
return simpleNumberString(quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,7 +466,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
"pk": "${pk}",
|
"pk": "${pk}",
|
||||||
"quantity": "${q}",
|
"quantity": "${q}",
|
||||||
},
|
},
|
||||||
"notes": notes ?? '',
|
"notes": notes ?? "",
|
||||||
},
|
},
|
||||||
expectedStatusCode: 200
|
expectedStatusCode: 200
|
||||||
);
|
);
|
||||||
@ -489,6 +474,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
return response.isValid();
|
return response.isValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor this once the server supports API metadata for this action
|
||||||
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
||||||
|
|
||||||
final bool result = await adjustStock(context, "/stock/count/", q, notes: notes);
|
final bool result = await adjustStock(context, "/stock/count/", q, notes: notes);
|
||||||
@ -496,6 +482,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor this once the server supports API metadata for this action
|
||||||
Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
|
Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
|
||||||
|
|
||||||
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
|
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
|
||||||
@ -503,6 +490,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor this once the server supports API metadata for this action
|
||||||
Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
|
Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
|
||||||
|
|
||||||
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
|
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
|
||||||
@ -510,6 +498,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor this once the server supports API metadata for this action
|
||||||
Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
|
Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
|
||||||
if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) {
|
if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) {
|
||||||
quantity = this.quantity;
|
quantity = this.quantity;
|
||||||
@ -535,10 +524,14 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
class InvenTreeStockLocation extends InvenTreeModel {
|
class InvenTreeStockLocation extends InvenTreeModel {
|
||||||
|
|
||||||
|
InvenTreeStockLocation() : super();
|
||||||
|
|
||||||
|
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get URL => "stock/location/";
|
String get URL => "stock/location/";
|
||||||
|
|
||||||
String get pathstring => jsondata['pathstring'] ?? '';
|
String get pathstring => (jsondata["pathstring"] ?? "") as String;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> formFields() {
|
Map<String, dynamic> formFields() {
|
||||||
@ -551,13 +544,13 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
|||||||
|
|
||||||
String get parentpathstring {
|
String get parentpathstring {
|
||||||
// TODO - Drive the refactor tractor through this
|
// TODO - Drive the refactor tractor through this
|
||||||
List<String> psplit = pathstring.split('/');
|
List<String> psplit = pathstring.split("/");
|
||||||
|
|
||||||
if (psplit.length > 0) {
|
if (psplit.isNotEmpty) {
|
||||||
psplit.removeLast();
|
psplit.removeLast();
|
||||||
}
|
}
|
||||||
|
|
||||||
String p = psplit.join('/');
|
String p = psplit.join("/");
|
||||||
|
|
||||||
if (p.isEmpty) {
|
if (p.isEmpty) {
|
||||||
p = "Top level stock location";
|
p = "Top level stock location";
|
||||||
@ -566,11 +559,7 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get itemcount => jsondata['items'] ?? 0;
|
int get itemcount => (jsondata["items"] ?? 0) as int;
|
||||||
|
|
||||||
InvenTreeStockLocation() : super();
|
|
||||||
|
|
||||||
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import "package:flutter_gen/gen_l10n/app_localizations.dart";
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations_en.dart';
|
import "package:flutter_gen/gen_l10n/app_localizations_en.dart";
|
||||||
|
|
||||||
import 'package:one_context/one_context.dart';
|
import "package:one_context/one_context.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
// Shortcut function to reduce boilerplate!
|
// Shortcut function to reduce boilerplate!
|
||||||
I18N L10()
|
I18N L10()
|
||||||
|
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_localizations/flutter_localizations.dart';
|
import "package:flutter_gen/gen_l10n/app_localizations.dart";
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/widget/home.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:one_context/one_context.dart";
|
||||||
import 'package:one_context/one_context.dart';
|
import "package:package_info_plus/package_info_plus.dart";
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import "package:flutter/foundation.dart";
|
||||||
|
import "package:sentry_flutter/sentry_flutter.dart";
|
||||||
|
|
||||||
import 'dsn.dart';
|
import "package:inventree/inventree/sentry.dart";
|
||||||
|
import "package:inventree/dsn.dart";
|
||||||
import 'package:flutter/foundation.dart';
|
import "package:inventree/widget/home.dart";
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
@ -75,24 +74,24 @@ class InvenTreeApp extends StatelessWidget {
|
|||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
supportedLocales: [
|
supportedLocales: [
|
||||||
const Locale('de', ''), // German
|
const Locale("de", ""), // German
|
||||||
const Locale('el', ''), // Greek
|
const Locale("el", ""), // Greek
|
||||||
const Locale('en', ''), // English
|
const Locale("en", ""), // English
|
||||||
const Locale('es', ''), // Spanish
|
const Locale("es", ""), // Spanish
|
||||||
const Locale('fr', ''), // French
|
const Locale("fr", ""), // French
|
||||||
const Locale('he', ''), // Hebrew
|
const Locale("he", ""), // Hebrew
|
||||||
const Locale('it', ''), // Italian
|
const Locale("it", ""), // Italian
|
||||||
const Locale('ja', ''), // Japanese
|
const Locale("ja", ""), // Japanese
|
||||||
const Locale('ko', ''), // Korean
|
const Locale("ko", ""), // Korean
|
||||||
const Locale('nl', ''), // Dutch
|
const Locale("nl", ""), // Dutch
|
||||||
const Locale('no', ''), // Norwegian
|
const Locale("no", ""), // Norwegian
|
||||||
const Locale('pl', ''), // Polish
|
const Locale("pl", ""), // Polish
|
||||||
const Locale('ru', ''), // Russian
|
const Locale("ru", ""), // Russian
|
||||||
const Locale('sv', ''), // Swedish
|
const Locale("sv", ""), // Swedish
|
||||||
const Locale('th', ''), // Thai
|
const Locale("th", ""), // Thai
|
||||||
const Locale('tr', ''), // Turkish
|
const Locale("tr", ""), // Turkish
|
||||||
const Locale('vi', ''), // Vietnamese
|
const Locale("vi", ""), // Vietnamese
|
||||||
const Locale('zh-CN', ''), // Chinese
|
const Locale("zh-CN", ""), // Chinese
|
||||||
],
|
],
|
||||||
|
|
||||||
);
|
);
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import "dart:async";
|
||||||
import 'package:sembast/sembast.dart';
|
|
||||||
import 'package:sembast/sembast_io.dart';
|
import "package:path_provider/path_provider.dart";
|
||||||
import 'package:path/path.dart';
|
import "package:sembast/sembast.dart";
|
||||||
import 'dart:async';
|
import "package:sembast/sembast_io.dart";
|
||||||
|
import "package:path/path.dart";
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Class for storing InvenTree preferences in a NoSql DB
|
* Class for storing InvenTree preferences in a NoSql DB
|
||||||
*/
|
*/
|
||||||
class InvenTreePreferencesDB {
|
class InvenTreePreferencesDB {
|
||||||
|
|
||||||
|
InvenTreePreferencesDB._();
|
||||||
|
|
||||||
static final InvenTreePreferencesDB _singleton = InvenTreePreferencesDB._();
|
static final InvenTreePreferencesDB _singleton = InvenTreePreferencesDB._();
|
||||||
|
|
||||||
static InvenTreePreferencesDB get instance => _singleton;
|
static InvenTreePreferencesDB get instance => _singleton;
|
||||||
|
|
||||||
InvenTreePreferencesDB._();
|
|
||||||
|
|
||||||
Completer<Database> _dbOpenCompleter = Completer();
|
Completer<Database> _dbOpenCompleter = Completer();
|
||||||
|
|
||||||
bool isOpen = false;
|
bool isOpen = false;
|
||||||
@ -34,7 +36,7 @@ class InvenTreePreferencesDB {
|
|||||||
return _dbOpenCompleter.future;
|
return _dbOpenCompleter.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _openDatabase() async {
|
Future<void> _openDatabase() async {
|
||||||
// Get a platform-specific directory where persistent app data can be stored
|
// Get a platform-specific directory where persistent app data can be stored
|
||||||
final appDocumentDir = await getApplicationDocumentsDirectory();
|
final appDocumentDir = await getApplicationDocumentsDirectory();
|
||||||
|
|
||||||
@ -43,7 +45,7 @@ class InvenTreePreferencesDB {
|
|||||||
print("Path: ${appDocumentDir.path}");
|
print("Path: ${appDocumentDir.path}");
|
||||||
|
|
||||||
// Path with the form: /platform-specific-directory/demo.db
|
// Path with the form: /platform-specific-directory/demo.db
|
||||||
final dbPath = join(appDocumentDir.path, 'InvenTreeSettings.db');
|
final dbPath = join(appDocumentDir.path, "InvenTreeSettings.db");
|
||||||
|
|
||||||
final database = await databaseFactoryIo.openDatabase(dbPath);
|
final database = await databaseFactoryIo.openDatabase(dbPath);
|
||||||
|
|
||||||
@ -54,8 +56,14 @@ class InvenTreePreferencesDB {
|
|||||||
|
|
||||||
class InvenTreePreferences {
|
class InvenTreePreferences {
|
||||||
|
|
||||||
|
factory InvenTreePreferences() {
|
||||||
|
return _api;
|
||||||
|
}
|
||||||
|
|
||||||
|
InvenTreePreferences._internal();
|
||||||
|
|
||||||
/* The following settings are not stored to persistent storage,
|
/* The following settings are not stored to persistent storage,
|
||||||
* instead they are only used as 'session preferences'.
|
* instead they are only used as "session preferences".
|
||||||
* They are kept here as a convenience only.
|
* They are kept here as a convenience only.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -72,11 +80,6 @@ class InvenTreePreferences {
|
|||||||
bool expandStockList = true;
|
bool expandStockList = true;
|
||||||
|
|
||||||
// Ensure we only ever create a single instance of the preferences class
|
// Ensure we only ever create a single instance of the preferences class
|
||||||
static final InvenTreePreferences _api = new InvenTreePreferences._internal();
|
static final InvenTreePreferences _api = InvenTreePreferences._internal();
|
||||||
|
|
||||||
factory InvenTreePreferences() {
|
|
||||||
return _api;
|
|
||||||
}
|
|
||||||
|
|
||||||
InvenTreePreferences._internal();
|
|
||||||
}
|
}
|
@ -1,22 +1,22 @@
|
|||||||
import 'package:inventree/api.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:inventree/app_colors.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
import 'package:inventree/settings/release.dart';
|
import "package:inventree/settings/release.dart";
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:flutter/services.dart';
|
import "package:flutter/services.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import "package:package_info_plus/package_info_plus.dart";
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
class InvenTreeAboutWidget extends StatelessWidget {
|
class InvenTreeAboutWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
const InvenTreeAboutWidget(this.info) : super();
|
||||||
|
|
||||||
final PackageInfo info;
|
final PackageInfo info;
|
||||||
|
|
||||||
InvenTreeAboutWidget(this.info) : super();
|
Future <void> _releaseNotes(BuildContext context) async {
|
||||||
|
|
||||||
void _releaseNotes(BuildContext context) async {
|
|
||||||
|
|
||||||
// Load release notes from external file
|
// Load release notes from external file
|
||||||
String notes = await rootBundle.loadString("assets/release_notes.md");
|
String notes = await rootBundle.loadString("assets/release_notes.md");
|
||||||
@ -27,7 +27,7 @@ class InvenTreeAboutWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _credits(BuildContext context) async {
|
Future <void> _credits(BuildContext context) async {
|
||||||
|
|
||||||
String notes = await rootBundle.loadString("assets/credits.md");
|
String notes = await rootBundle.loadString("assets/credits.md");
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
|
|
||||||
import 'package:inventree/app_settings.dart';
|
import "package:inventree/app_settings.dart";
|
||||||
|
|
||||||
class InvenTreeAppSettingsWidget extends StatefulWidget {
|
class InvenTreeAppSettingsWidget extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -15,10 +15,10 @@ class InvenTreeAppSettingsWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
|
class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
|
||||||
|
|
||||||
final GlobalKey<_InvenTreeAppSettingsState> _settingsKey = GlobalKey<_InvenTreeAppSettingsState>();
|
|
||||||
|
|
||||||
_InvenTreeAppSettingsState();
|
_InvenTreeAppSettingsState();
|
||||||
|
|
||||||
|
final GlobalKey<_InvenTreeAppSettingsState> _settingsKey = GlobalKey<_InvenTreeAppSettingsState>();
|
||||||
|
|
||||||
bool barcodeSounds = true;
|
bool barcodeSounds = true;
|
||||||
bool serverSounds = true;
|
bool serverSounds = true;
|
||||||
bool partSubcategory = false;
|
bool partSubcategory = false;
|
||||||
@ -31,7 +31,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
|
|||||||
loadSettings();
|
loadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadSettings() async {
|
Future <void> loadSettings() async {
|
||||||
barcodeSounds = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
|
barcodeSounds = await InvenTreeSettingsManager().getValue("barcodeSounds", true) as bool;
|
||||||
serverSounds = await InvenTreeSettingsManager().getValue("serverSounds", true) as bool;
|
serverSounds = await InvenTreeSettingsManager().getValue("serverSounds", true) as bool;
|
||||||
|
|
||||||
@ -42,35 +42,35 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setBarcodeSounds(bool en) async {
|
Future <void> setBarcodeSounds(bool en) async {
|
||||||
|
|
||||||
await InvenTreeSettingsManager().setValue("barcodeSounds", en);
|
await InvenTreeSettingsManager().setValue("barcodeSounds", en);
|
||||||
barcodeSounds = await InvenTreeSettingsManager().getValue("barcodeSounds", true);
|
barcodeSounds = await InvenTreeSettingsManager().getBool("barcodeSounds", true);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setServerSounds(bool en) async {
|
Future <void> setServerSounds(bool en) async {
|
||||||
|
|
||||||
await InvenTreeSettingsManager().setValue("serverSounds", en);
|
await InvenTreeSettingsManager().setValue("serverSounds", en);
|
||||||
serverSounds = await InvenTreeSettingsManager().getValue("serverSounds", true);
|
serverSounds = await InvenTreeSettingsManager().getBool("serverSounds", true);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPartSubcategory(bool en) async {
|
Future <void> setPartSubcategory(bool en) async {
|
||||||
await InvenTreeSettingsManager().setValue("partSubcategory", en);
|
await InvenTreeSettingsManager().setValue("partSubcategory", en);
|
||||||
partSubcategory = await InvenTreeSettingsManager().getValue("partSubcategory", true);
|
partSubcategory = await InvenTreeSettingsManager().getBool("partSubcategory", true);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setStockSublocation(bool en) async {
|
Future <void> setStockSublocation(bool en) async {
|
||||||
await InvenTreeSettingsManager().setValue("stockSublocation", en);
|
await InvenTreeSettingsManager().setValue("stockSublocation", en);
|
||||||
stockSublocation = await InvenTreeSettingsManager().getValue("stockSublocation", true);
|
stockSublocation = await InvenTreeSettingsManager().getBool("stockSublocation", true);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import 'package:inventree/app_colors.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/widget/fields.dart';
|
|
||||||
import 'package:inventree/widget/spinner.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:inventree/widget/dialogs.dart";
|
||||||
|
import "package:inventree/widget/spinner.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/api.dart";
|
||||||
import '../api.dart';
|
import "package:inventree/user_profile.dart";
|
||||||
import '../user_profile.dart';
|
|
||||||
|
|
||||||
class InvenTreeLoginSettingsWidget extends StatefulWidget {
|
class InvenTreeLoginSettingsWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -20,17 +17,15 @@ class InvenTreeLoginSettingsWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||||
|
|
||||||
final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>();
|
|
||||||
|
|
||||||
final GlobalKey<FormState> _addProfileKey = new GlobalKey<FormState>();
|
|
||||||
|
|
||||||
List<UserProfile> profiles = [];
|
|
||||||
|
|
||||||
_InvenTreeLoginSettingsState() {
|
_InvenTreeLoginSettingsState() {
|
||||||
_reload();
|
_reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _reload() async {
|
final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>();
|
||||||
|
|
||||||
|
List<UserProfile> profiles = [];
|
||||||
|
|
||||||
|
Future <void> _reload() async {
|
||||||
|
|
||||||
profiles = await UserProfileDBManager().getAllProfiles();
|
profiles = await UserProfileDBManager().getAllProfiles();
|
||||||
|
|
||||||
@ -40,17 +35,6 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
|
|
||||||
void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) {
|
void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) {
|
||||||
|
|
||||||
var _name;
|
|
||||||
var _server;
|
|
||||||
var _username;
|
|
||||||
var _password;
|
|
||||||
|
|
||||||
UserProfile? profile;
|
|
||||||
|
|
||||||
if (userProfile != null) {
|
|
||||||
profile = userProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@ -61,7 +45,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectProfile(BuildContext context, UserProfile profile) async {
|
Future <void> _selectProfile(BuildContext context, UserProfile profile) async {
|
||||||
|
|
||||||
// Disconnect InvenTree
|
// Disconnect InvenTree
|
||||||
InvenTreeAPI().disconnectFromServer();
|
InvenTreeAPI().disconnectFromServer();
|
||||||
@ -84,42 +68,24 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
_reload();
|
_reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deleteProfile(UserProfile profile) async {
|
Future <void> _deleteProfile(UserProfile profile) async {
|
||||||
|
|
||||||
await UserProfileDBManager().deleteProfile(profile);
|
await UserProfileDBManager().deleteProfile(profile);
|
||||||
|
|
||||||
_reload();
|
_reload();
|
||||||
|
|
||||||
if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
|
if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? "")) {
|
||||||
InvenTreeAPI().disconnectFromServer();
|
InvenTreeAPI().disconnectFromServer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateProfile(UserProfile? profile) async {
|
|
||||||
|
|
||||||
if (profile == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_reload();
|
|
||||||
|
|
||||||
if (InvenTreeAPI().isConnected() && InvenTreeAPI().profile != null && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
|
|
||||||
// Attempt server login (this will load the newly selected profile
|
|
||||||
|
|
||||||
InvenTreeAPI().connectToServer().then((result) {
|
|
||||||
_reload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Widget? _getProfileIcon(UserProfile profile) {
|
Widget? _getProfileIcon(UserProfile profile) {
|
||||||
|
|
||||||
// Not selected? No icon for you!
|
// Not selected? No icon for you!
|
||||||
if (!profile.selected) return null;
|
if (!profile.selected) return null;
|
||||||
|
|
||||||
// Selected, but (for some reason) not the same as the API...
|
// Selected, but (for some reason) not the same as the API...
|
||||||
if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
|
if ((InvenTreeAPI().profile?.key ?? "") != profile.key) {
|
||||||
return FaIcon(
|
return FaIcon(
|
||||||
FontAwesomeIcons.questionCircle,
|
FontAwesomeIcons.questionCircle,
|
||||||
color: COLOR_WARNING
|
color: COLOR_WARNING
|
||||||
@ -150,7 +116,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
|
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
|
|
||||||
if (profiles.length > 0) {
|
if (profiles.isNotEmpty) {
|
||||||
for (int idx = 0; idx < profiles.length; idx++) {
|
for (int idx = 0; idx < profiles.length; idx++) {
|
||||||
UserProfile profile = profiles[idx];
|
UserProfile profile = profiles[idx];
|
||||||
|
|
||||||
@ -253,9 +219,9 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
|
|
||||||
class ProfileEditWidget extends StatefulWidget {
|
class ProfileEditWidget extends StatefulWidget {
|
||||||
|
|
||||||
UserProfile? profile;
|
const ProfileEditWidget(this.profile) : super();
|
||||||
|
|
||||||
ProfileEditWidget(this.profile) : super();
|
final UserProfile? profile;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ProfileEditState createState() => _ProfileEditState(profile);
|
_ProfileEditState createState() => _ProfileEditState(profile);
|
||||||
@ -263,11 +229,11 @@ class ProfileEditWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _ProfileEditState extends State<ProfileEditWidget> {
|
class _ProfileEditState extends State<ProfileEditWidget> {
|
||||||
|
|
||||||
UserProfile? profile;
|
|
||||||
|
|
||||||
_ProfileEditState(this.profile) : super();
|
_ProfileEditState(this.profile) : super();
|
||||||
|
|
||||||
final formKey = new GlobalKey<FormState>();
|
UserProfile? profile;
|
||||||
|
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
String name = "";
|
String name = "";
|
||||||
String server = "";
|
String server = "";
|
||||||
@ -375,7 +341,7 @@ class _ProfileEditState extends State<ProfileEditWidget> {
|
|||||||
|
|
||||||
if (uri.hasScheme) {
|
if (uri.hasScheme) {
|
||||||
print("Scheme: ${uri.scheme}");
|
print("Scheme: ${uri.scheme}");
|
||||||
if (!(["http", "https"].contains(uri.scheme.toLowerCase()))) {
|
if (!["http", "https"].contains(uri.scheme.toLowerCase())) {
|
||||||
return L10().serverStart;
|
return L10().serverStart;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import "package:flutter_markdown/flutter_markdown.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
|
|
||||||
class ReleaseNotesWidget extends StatelessWidget {
|
class ReleaseNotesWidget extends StatelessWidget {
|
||||||
|
|
||||||
final String releaseNotes;
|
const ReleaseNotesWidget(this.releaseNotes);
|
||||||
|
|
||||||
ReleaseNotesWidget(this.releaseNotes);
|
final String releaseNotes;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build (BuildContext context) {
|
Widget build (BuildContext context) {
|
||||||
@ -27,9 +27,9 @@ class ReleaseNotesWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class CreditsWidget extends StatelessWidget {
|
class CreditsWidget extends StatelessWidget {
|
||||||
|
|
||||||
final String credits;
|
const CreditsWidget(this.credits);
|
||||||
|
|
||||||
CreditsWidget(this.credits);
|
final String credits;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build (BuildContext context) {
|
Widget build (BuildContext context) {
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import 'package:inventree/app_colors.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
import 'package:inventree/settings/about.dart';
|
import "package:inventree/settings/about.dart";
|
||||||
import 'package:inventree/settings/app_settings.dart';
|
import "package:inventree/settings/app_settings.dart";
|
||||||
import 'package:inventree/settings/login.dart';
|
import "package:inventree/settings/login.dart";
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
import 'package:inventree/widget/submit_feedback.dart';
|
import "package:inventree/widget/submit_feedback.dart";
|
||||||
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import "package:url_launcher/url_launcher.dart";
|
||||||
|
|
||||||
import 'login.dart';
|
import "package:package_info_plus/package_info_plus.dart";
|
||||||
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
|
||||||
|
|
||||||
class InvenTreeSettingsWidget extends StatefulWidget {
|
class InvenTreeSettingsWidget extends StatefulWidget {
|
||||||
// InvenTree settings view
|
// InvenTree settings view
|
||||||
@ -95,30 +93,30 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _openDocs() async {
|
Future <void> _openDocs() async {
|
||||||
if (await canLaunch(docsUrl)) {
|
if (await canLaunch(docsUrl)) {
|
||||||
await launch(docsUrl);
|
await launch(docsUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _translate() async {
|
Future <void> _translate() async {
|
||||||
final String url = "https://crowdin.com/project/inventree";
|
const String url = "https://crowdin.com/project/inventree";
|
||||||
|
|
||||||
if (await canLaunch(url)) {
|
if (await canLaunch(url)) {
|
||||||
await launch(url);
|
await launch(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editServerSettings() async {
|
Future <void> _editServerSettings() async {
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget()));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editAppSettings() async {
|
Future <void> _editAppSettings() async {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeAppSettingsWidget()));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeAppSettingsWidget()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _about() async {
|
Future <void> _about() async {
|
||||||
|
|
||||||
PackageInfo.fromPlatform().then((PackageInfo info) {
|
PackageInfo.fromPlatform().then((PackageInfo info) {
|
||||||
Navigator.push(context,
|
Navigator.push(context,
|
||||||
@ -126,7 +124,7 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitFeedback(BuildContext context) async {
|
Future <void> _submitFeedback(BuildContext context) async {
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
/*
|
/*
|
||||||
* Class for InvenTree user / login details
|
* Class for InvenTree user / login details
|
||||||
*/
|
*/
|
||||||
import 'package:sembast/sembast.dart';
|
import "package:sembast/sembast.dart";
|
||||||
import 'preferences.dart';
|
import "preferences.dart";
|
||||||
|
|
||||||
class UserProfile {
|
class UserProfile {
|
||||||
|
|
||||||
@ -16,6 +16,15 @@ class UserProfile {
|
|||||||
this.selected = false,
|
this.selected = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
|
||||||
|
key: key,
|
||||||
|
name: json["name"] as String,
|
||||||
|
server: json["server"] as String,
|
||||||
|
username: json["username"] as String,
|
||||||
|
password: json["password"] as String,
|
||||||
|
selected: isSelected,
|
||||||
|
);
|
||||||
|
|
||||||
// ID of the profile
|
// ID of the profile
|
||||||
int? key;
|
int? key;
|
||||||
|
|
||||||
@ -36,15 +45,6 @@ class UserProfile {
|
|||||||
// User ID (will be provided by the server on log-in)
|
// User ID (will be provided by the server on log-in)
|
||||||
int user_id = -1;
|
int user_id = -1;
|
||||||
|
|
||||||
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
|
|
||||||
key: key,
|
|
||||||
name: json['name'],
|
|
||||||
server: json['server'],
|
|
||||||
username: json['username'],
|
|
||||||
password: json['password'],
|
|
||||||
selected: isSelected,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"name": name,
|
"name": name,
|
||||||
"server": server,
|
"server": server,
|
||||||
@ -62,7 +62,7 @@ class UserProfileDBManager {
|
|||||||
|
|
||||||
final store = StoreRef("profiles");
|
final store = StoreRef("profiles");
|
||||||
|
|
||||||
Future<Database> get _db async => await InvenTreePreferencesDB.instance.database;
|
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
|
||||||
|
|
||||||
Future<bool> profileNameExists(String name) async {
|
Future<bool> profileNameExists(String name) async {
|
||||||
|
|
||||||
@ -70,10 +70,10 @@ class UserProfileDBManager {
|
|||||||
|
|
||||||
final profiles = await store.find(await _db, finder: finder);
|
final profiles = await store.find(await _db, finder: finder);
|
||||||
|
|
||||||
return profiles.length > 0;
|
return profiles.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future addProfile(UserProfile profile) async {
|
Future<void> addProfile(UserProfile profile) async {
|
||||||
|
|
||||||
// Check if a profile already exists with the name
|
// Check if a profile already exists with the name
|
||||||
final bool exists = await profileNameExists(profile.name);
|
final bool exists = await profileNameExists(profile.name);
|
||||||
@ -83,7 +83,7 @@ class UserProfileDBManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int key = await store.add(await _db, profile.toJson());
|
int key = await store.add(await _db, profile.toJson()) as int;
|
||||||
|
|
||||||
print("Added user profile <${key}> - '${profile.name}'");
|
print("Added user profile <${key}> - '${profile.name}'");
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ class UserProfileDBManager {
|
|||||||
profile.key = key;
|
profile.key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future selectProfile(int key) async {
|
Future<void> selectProfile(int key) async {
|
||||||
/*
|
/*
|
||||||
* Mark the particular profile as selected
|
* Mark the particular profile as selected
|
||||||
*/
|
*/
|
||||||
@ -101,7 +101,7 @@ class UserProfileDBManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future updateProfile(UserProfile profile) async {
|
Future<void> updateProfile(UserProfile profile) async {
|
||||||
|
|
||||||
if (profile.key == null) {
|
if (profile.key == null) {
|
||||||
await addProfile(profile);
|
await addProfile(profile);
|
||||||
@ -115,7 +115,7 @@ class UserProfileDBManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future deleteProfile(UserProfile profile) async {
|
Future<void> deleteProfile(UserProfile profile) async {
|
||||||
await store.record(profile.key).delete(await _db);
|
await store.record(profile.key).delete(await _db);
|
||||||
print("Deleted user profile <${profile.key}> - '${profile.name}'");
|
print("Deleted user profile <${profile.key}> - '${profile.name}'");
|
||||||
}
|
}
|
||||||
@ -135,8 +135,8 @@ class UserProfileDBManager {
|
|||||||
|
|
||||||
if (profiles[idx].key is int && profiles[idx].key == selected) {
|
if (profiles[idx].key is int && profiles[idx].key == selected) {
|
||||||
return UserProfile.fromJson(
|
return UserProfile.fromJson(
|
||||||
profiles[idx].key,
|
profiles[idx].key as int,
|
||||||
profiles[idx].value,
|
profiles[idx].value as Map<String, dynamic>,
|
||||||
profiles[idx].key == selected,
|
profiles[idx].key == selected,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -161,8 +161,8 @@ class UserProfileDBManager {
|
|||||||
if (profiles[idx].key is int) {
|
if (profiles[idx].key is int) {
|
||||||
profileList.add(
|
profileList.add(
|
||||||
UserProfile.fromJson(
|
UserProfile.fromJson(
|
||||||
profiles[idx].key,
|
profiles[idx].key as int,
|
||||||
profiles[idx].value,
|
profiles[idx].value as Map<String, dynamic>,
|
||||||
profiles[idx].key == selected,
|
profiles[idx].key == selected,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
27
lib/widget/back.dart
Normal file
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:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/app_colors.dart';
|
|
||||||
import 'package:inventree/app_settings.dart';
|
|
||||||
import 'package:inventree/inventree/part.dart';
|
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
|
||||||
import 'package:inventree/widget/progress.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/api.dart";
|
||||||
|
import "package:inventree/app_colors.dart";
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:inventree/widget/part_list.dart";
|
||||||
|
import "package:inventree/widget/progress.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/widget/part_detail.dart";
|
||||||
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
|
||||||
import 'package:inventree/widget/part_detail.dart';
|
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
|
||||||
import 'package:inventree/widget/paginator.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
|
|
||||||
class CategoryDisplayWidget extends StatefulWidget {
|
class CategoryDisplayWidget extends StatefulWidget {
|
||||||
|
|
||||||
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
const CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
final InvenTreePartCategory? category;
|
final InvenTreePartCategory? category;
|
||||||
|
|
||||||
@ -32,6 +27,7 @@ class CategoryDisplayWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||||
|
|
||||||
|
_CategoryDisplayState(this.category);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().partCategory;
|
String getAppBarTitle(BuildContext context) => L10().partCategory;
|
||||||
@ -41,7 +37,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
|
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
if ((category != null) && InvenTreeAPI().checkPermission('part_category', 'change')) {
|
if ((category != null) && InvenTreeAPI().checkPermission("part_category", "change")) {
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
@ -74,8 +70,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_CategoryDisplayState(this.category);
|
|
||||||
|
|
||||||
// The local InvenTreePartCategory object
|
// The local InvenTreePartCategory object
|
||||||
final InvenTreePartCategory? category;
|
final InvenTreePartCategory? category;
|
||||||
|
|
||||||
@ -199,7 +193,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
tiles.add(progressIndicator());
|
tiles.add(progressIndicator());
|
||||||
} else if (_subcategories.length == 0) {
|
} else if (_subcategories.isEmpty) {
|
||||||
tiles.add(ListTile(
|
tiles.add(ListTile(
|
||||||
title: Text(L10().noSubcategories),
|
title: Text(L10().noSubcategories),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
@ -224,7 +218,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
data: {
|
data: {
|
||||||
"parent": (pk > 0) ? pk : null,
|
"parent": (pk > 0) ? pk : null,
|
||||||
},
|
},
|
||||||
onSuccess: (data) async {
|
onSuccess: (result) async {
|
||||||
|
|
||||||
|
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||||
|
|
||||||
if (data.containsKey("pk")) {
|
if (data.containsKey("pk")) {
|
||||||
var cat = InvenTreePartCategory.fromJson(data);
|
var cat = InvenTreePartCategory.fromJson(data);
|
||||||
@ -252,7 +248,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
data: {
|
data: {
|
||||||
"category": (pk > 0) ? pk : null
|
"category": (pk > 0) ? pk : null
|
||||||
},
|
},
|
||||||
onSuccess: (data) async {
|
onSuccess: (result) async {
|
||||||
|
|
||||||
|
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||||
|
|
||||||
if (data.containsKey("pk")) {
|
if (data.containsKey("pk")) {
|
||||||
var part = InvenTreePart.fromJson(data);
|
var part = InvenTreePart.fromJson(data);
|
||||||
@ -274,7 +272,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
getCategoryDescriptionCard(extra: false),
|
getCategoryDescriptionCard(extra: false),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('part', 'add')) {
|
if (InvenTreeAPI().checkPermission("part", "add")) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().categoryCreate),
|
title: Text(L10().categoryCreate),
|
||||||
@ -298,7 +296,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tiles.length == 0) {
|
if (tiles.isEmpty) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -327,7 +325,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return PaginatedPartList(
|
return PaginatedPartList(
|
||||||
{"category": "${category?.pk ?? null}"},
|
{
|
||||||
|
"category": "${category?.pk ?? 'null'}"
|
||||||
|
},
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return ListView(
|
return ListView(
|
||||||
@ -344,9 +344,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
* Builder for displaying a list of PartCategory objects
|
* Builder for displaying a list of PartCategory objects
|
||||||
*/
|
*/
|
||||||
class SubcategoryList extends StatelessWidget {
|
class SubcategoryList extends StatelessWidget {
|
||||||
final List<InvenTreePartCategory> _categories;
|
|
||||||
|
|
||||||
SubcategoryList(this._categories);
|
const SubcategoryList(this._categories);
|
||||||
|
|
||||||
|
final List<InvenTreePartCategory> _categories;
|
||||||
|
|
||||||
void _openCategory(BuildContext context, int pk) {
|
void _openCategory(BuildContext context, int pk) {
|
||||||
|
|
||||||
@ -381,170 +382,3 @@ class SubcategoryList extends StatelessWidget {
|
|||||||
itemBuilder: _build, itemCount: _categories.length);
|
itemBuilder: _build, itemCount: _categories.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Widget for displaying a list of Part objects within a PartCategory display.
|
|
||||||
*
|
|
||||||
* Uses server-side pagination for snappy results
|
|
||||||
*/
|
|
||||||
|
|
||||||
class PaginatedPartList extends StatefulWidget {
|
|
||||||
|
|
||||||
final Map<String, String> filters;
|
|
||||||
|
|
||||||
Function(int)? onTotalChanged;
|
|
||||||
|
|
||||||
PaginatedPartList(this.filters, {this.onTotalChanged});
|
|
||||||
|
|
||||||
@override
|
|
||||||
_PaginatedPartListState createState() => _PaginatedPartListState(filters, onTotalChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class _PaginatedPartListState extends State<PaginatedPartList> {
|
|
||||||
|
|
||||||
static const _pageSize = 25;
|
|
||||||
|
|
||||||
String _searchTerm = "";
|
|
||||||
|
|
||||||
Function(int)? onTotalChanged;
|
|
||||||
|
|
||||||
final Map<String, String> filters;
|
|
||||||
|
|
||||||
_PaginatedPartListState(this.filters, this.onTotalChanged);
|
|
||||||
|
|
||||||
final PagingController<int, InvenTreePart> _pagingController = PagingController(firstPageKey: 0);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_pagingController.addPageRequestListener((pageKey) {
|
|
||||||
_fetchPage(pageKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
int resultCount = 0;
|
|
||||||
|
|
||||||
Future<void> _fetchPage(int pageKey) async {
|
|
||||||
try {
|
|
||||||
|
|
||||||
Map<String, String> params = filters;
|
|
||||||
|
|
||||||
params["search"] = _searchTerm;
|
|
||||||
|
|
||||||
final bool cascade = await InvenTreeSettingsManager().getValue("partSubcategory", true);
|
|
||||||
params["cascade"] = "${cascade}";
|
|
||||||
|
|
||||||
final page = await InvenTreePart().listPaginated(_pageSize, pageKey, filters: params);
|
|
||||||
int pageLength = page?.length ?? 0;
|
|
||||||
int pageCount = page?.count ?? 0;
|
|
||||||
|
|
||||||
final isLastPage = pageLength < _pageSize;
|
|
||||||
|
|
||||||
// Construct a list of part objects
|
|
||||||
List<InvenTreePart> parts = [];
|
|
||||||
|
|
||||||
if (page != null) {
|
|
||||||
for (var result in page.results) {
|
|
||||||
if (result is InvenTreePart) {
|
|
||||||
parts.add(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLastPage) {
|
|
||||||
_pagingController.appendLastPage(parts);
|
|
||||||
} else {
|
|
||||||
final int nextPageKey = pageKey + pageLength;
|
|
||||||
_pagingController.appendPage(parts, nextPageKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onTotalChanged != null) {
|
|
||||||
onTotalChanged!(pageCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
resultCount = pageCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
print("Error! - ${error.toString()}");
|
|
||||||
_pagingController.error = error;
|
|
||||||
|
|
||||||
sentryReportError(error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openPart(BuildContext context, int pk) {
|
|
||||||
// Attempt to load the part information
|
|
||||||
InvenTreePart().get(pk).then((var part) {
|
|
||||||
if (part is InvenTreePart) {
|
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPart(BuildContext context, InvenTreePart part) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(part.fullname),
|
|
||||||
subtitle: Text("${part.description}"),
|
|
||||||
trailing: Text("${part.inStockString}"),
|
|
||||||
leading: InvenTreeAPI().getImage(
|
|
||||||
part.thumbnail,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_openPart(context, part.pk);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final TextEditingController searchController = TextEditingController();
|
|
||||||
|
|
||||||
void updateSearchTerm() {
|
|
||||||
|
|
||||||
_searchTerm = searchController.text;
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
|
|
||||||
Expanded(
|
|
||||||
child: CustomScrollView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: ClampingScrollPhysics(),
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
slivers: [
|
|
||||||
PagedSliverList.separated(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate<InvenTreePart>(
|
|
||||||
itemBuilder: (context, item, index) {
|
|
||||||
return _buildPart(context, item);
|
|
||||||
},
|
|
||||||
noItemsFoundIndicatorBuilder: (context) {
|
|
||||||
return NoResultsWidget(L10().partNoResults);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
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.dart";
|
||||||
import 'package:inventree/api_form.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
import 'package:inventree/app_colors.dart';
|
import "package:inventree/inventree/company.dart";
|
||||||
import 'package:inventree/inventree/company.dart';
|
import "package:inventree/inventree/purchase_order.dart";
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import "package:inventree/widget/purchase_order_list.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
|
|
||||||
class CompanyDetailWidget extends StatefulWidget {
|
class CompanyDetailWidget extends StatefulWidget {
|
||||||
|
|
||||||
final InvenTreeCompany company;
|
const CompanyDetailWidget(this.company, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
CompanyDetailWidget(this.company, {Key? key}) : super(key: key);
|
final InvenTreeCompany company;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CompanyDetailState createState() => _CompanyDetailState(company);
|
_CompanyDetailState createState() => _CompanyDetailState(company);
|
||||||
@ -27,6 +29,8 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
|||||||
|
|
||||||
final InvenTreeCompany company;
|
final InvenTreeCompany company;
|
||||||
|
|
||||||
|
List<InvenTreePurchaseOrder> outstandingOrders = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().company;
|
String getAppBarTitle(BuildContext context) => L10().company;
|
||||||
|
|
||||||
@ -61,9 +65,13 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
|||||||
@override
|
@override
|
||||||
Future<void> request() async {
|
Future<void> request() async {
|
||||||
await company.reload();
|
await company.reload();
|
||||||
|
|
||||||
|
if (company.isSupplier) {
|
||||||
|
outstandingOrders = await company.getPurchaseOrders(outstanding: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void editCompany(BuildContext context) async {
|
Future <void> editCompany(BuildContext context) async {
|
||||||
|
|
||||||
company.editForm(
|
company.editForm(
|
||||||
context,
|
context,
|
||||||
@ -146,6 +154,37 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
|||||||
// TODO - Add list of purchase orders
|
// TODO - Add list of purchase orders
|
||||||
|
|
||||||
tiles.add(Divider());
|
tiles.add(Divider());
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().purchaseOrders),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.shoppingCart, color: COLOR_CLICK),
|
||||||
|
trailing: Text("${outstandingOrders.length}"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PurchaseOrderListWidget(
|
||||||
|
filters: {
|
||||||
|
"supplier": "${company.pk}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Display "supplied parts" count (click through to list of supplier parts)
|
||||||
|
/*
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().suppliedParts),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.shapes),
|
||||||
|
trailing: Text("${company.partSuppliedCount}"),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
if (company.isManufacturer) {
|
if (company.isManufacturer) {
|
||||||
|
@ -1,25 +1,22 @@
|
|||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:inventree/inventree/company.dart';
|
import "package:inventree/inventree/company.dart";
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
import "package:inventree/inventree/model.dart";
|
||||||
import 'package:inventree/widget/paginator.dart';
|
import "package:inventree/widget/paginator.dart";
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
import 'package:inventree/widget/company_detail.dart';
|
import "package:inventree/widget/company_detail.dart";
|
||||||
|
|
||||||
import '../l10.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class CompanyListWidget extends StatefulWidget {
|
class CompanyListWidget extends StatefulWidget {
|
||||||
|
|
||||||
CompanyListWidget(this.title, this.filters, {Key? key}) : super(key: key);
|
const CompanyListWidget(this.title, this.filters, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
String title;
|
final String title;
|
||||||
|
|
||||||
Map<String, String> filters;
|
final Map<String, String> filters;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CompanyListWidgetState createState() => _CompanyListWidgetState(title, filters);
|
_CompanyListWidgetState createState() => _CompanyListWidgetState(title, filters);
|
||||||
@ -49,103 +46,32 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
|
|||||||
|
|
||||||
class PaginatedCompanyList extends StatefulWidget {
|
class PaginatedCompanyList extends StatefulWidget {
|
||||||
|
|
||||||
PaginatedCompanyList(this.filters, {this.onTotalChanged});
|
const PaginatedCompanyList(this.filters, {this.onTotalChanged});
|
||||||
|
|
||||||
final Map<String, String> filters;
|
final Map<String, String> filters;
|
||||||
|
|
||||||
Function(int)? onTotalChanged;
|
final Function(int)? onTotalChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CompanyListState createState() => _CompanyListState(filters, onTotalChanged);
|
_CompanyListState createState() => _CompanyListState(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CompanyListState extends State<PaginatedCompanyList> {
|
class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> {
|
||||||
|
|
||||||
_CompanyListState(this.filters, this.onTotalChanged);
|
_CompanyListState(Map<String, String> filters) : super(filters);
|
||||||
|
|
||||||
static const _pageSize = 25;
|
|
||||||
|
|
||||||
String _searchTerm = "";
|
|
||||||
|
|
||||||
Function(int)? onTotalChanged;
|
|
||||||
|
|
||||||
final Map<String, String> filters;
|
|
||||||
|
|
||||||
final PagingController<int, InvenTreeCompany> _pagingController = PagingController(firstPageKey: 0);
|
|
||||||
|
|
||||||
final TextEditingController searchController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
||||||
_pagingController.addPageRequestListener((pageKey) {
|
|
||||||
_fetchPage(pageKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
super.initState();
|
final page = await InvenTreeCompany().listPaginated(limit, offset, filters: params);
|
||||||
|
|
||||||
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
int resultCount = 0;
|
InvenTreeCompany company = model as InvenTreeCompany;
|
||||||
|
|
||||||
Future<void> _fetchPage(int pageKey) async {
|
|
||||||
try {
|
|
||||||
Map<String, String> params = filters;
|
|
||||||
|
|
||||||
params["search"] = _searchTerm;
|
|
||||||
|
|
||||||
final page = await InvenTreeCompany().listPaginated(
|
|
||||||
_pageSize, pageKey, filters: params);
|
|
||||||
|
|
||||||
int pageLength = page?.length ?? 0;
|
|
||||||
int pageCount = page?.count ?? 0;
|
|
||||||
|
|
||||||
final isLastPage = pageLength < _pageSize;
|
|
||||||
|
|
||||||
List<InvenTreeCompany> companies = [];
|
|
||||||
|
|
||||||
if (page != null) {
|
|
||||||
for (var result in page.results) {
|
|
||||||
if (result is InvenTreeCompany) {
|
|
||||||
companies.add(result);
|
|
||||||
} else {
|
|
||||||
print(result.jsondata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLastPage) {
|
|
||||||
_pagingController.appendLastPage(companies);
|
|
||||||
} else {
|
|
||||||
final int nextPageKey = pageKey + pageLength;
|
|
||||||
_pagingController.appendPage(companies, nextPageKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onTotalChanged != null) {
|
|
||||||
onTotalChanged!(pageCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
resultCount = pageCount;
|
|
||||||
});
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
print("Error! - ${error.toString()}");
|
|
||||||
_pagingController.error = error;
|
|
||||||
|
|
||||||
sentryReportError(error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateSearchTerm() {
|
|
||||||
_searchTerm = searchController.text;
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCompany(BuildContext context, InvenTreeCompany company) {
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(company.name),
|
title: Text(company.name),
|
||||||
@ -160,36 +86,4 @@ class _CompanyListState extends State<PaginatedCompanyList> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
|
|
||||||
Expanded(
|
|
||||||
child: CustomScrollView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: ClampingScrollPhysics(),
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
slivers: [
|
|
||||||
PagedSliverList.separated(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate<InvenTreeCompany>(
|
|
||||||
itemBuilder: (context, item, index) {
|
|
||||||
return _buildCompany(context, item);
|
|
||||||
},
|
|
||||||
noItemsFoundIndicatorBuilder: (context) {
|
|
||||||
return NoResultsWidget(L10().companyNoResults);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
import 'package:inventree/app_settings.dart';
|
import "package:inventree/app_settings.dart";
|
||||||
import 'package:inventree/widget/snacks.dart';
|
import "package:inventree/widget/snacks.dart";
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import "package:audioplayers/audioplayers.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
import 'package:one_context/one_context.dart';
|
import "package:one_context/one_context.dart";
|
||||||
|
|
||||||
Future<void> confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {
|
Future<void> confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {
|
||||||
|
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
import 'package:inventree/api.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:inventree/barcode.dart';
|
import "package:inventree/barcode.dart";
|
||||||
import 'package:inventree/widget/company_list.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:inventree/widget/search.dart';
|
import "package:inventree/l10.dart";
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:inventree/l10.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/widget/category_display.dart';
|
import "package:inventree/settings/settings.dart";
|
||||||
import 'package:inventree/widget/location_display.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
|
import "package:inventree/widget/search.dart";
|
||||||
import 'package:inventree/settings/settings.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
|
|
||||||
class InvenTreeDrawer extends StatelessWidget {
|
class InvenTreeDrawer extends StatelessWidget {
|
||||||
|
|
||||||
final BuildContext context;
|
const InvenTreeDrawer(this.context);
|
||||||
|
|
||||||
InvenTreeDrawer(this.context);
|
final BuildContext context;
|
||||||
|
|
||||||
void _closeDrawer() {
|
void _closeDrawer() {
|
||||||
// Close the drawer
|
// Close the drawer
|
||||||
@ -29,7 +25,9 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
void _home() {
|
void _home() {
|
||||||
_closeDrawer();
|
_closeDrawer();
|
||||||
|
|
||||||
Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false);
|
while (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _search() {
|
void _search() {
|
||||||
@ -38,65 +36,25 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
|
|
||||||
_closeDrawer();
|
_closeDrawer();
|
||||||
|
|
||||||
showSearch(
|
Navigator.push(
|
||||||
context: context,
|
context,
|
||||||
delegate: PartSearchDelegate(context)
|
MaterialPageRoute(
|
||||||
|
builder: (context) => SearchWidget()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
//Navigator.push(context, MaterialPageRoute(builder: (context) => SearchWidget()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Launch the camera to scan a QR code.
|
* Launch the camera to scan a QR code.
|
||||||
* Upon successful scan, data are passed off to be decoded.
|
* Upon successful scan, data are passed off to be decoded.
|
||||||
*/
|
*/
|
||||||
void _scan() async {
|
Future <void> _scan() async {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
_closeDrawer();
|
_closeDrawer();
|
||||||
scanQrCode(context);
|
scanQrCode(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Display the top-level PartCategory list
|
|
||||||
*/
|
|
||||||
void _showParts() {
|
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
|
||||||
|
|
||||||
_closeDrawer();
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Display the top-level StockLocation list
|
|
||||||
*/
|
|
||||||
void _showStock() {
|
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
|
||||||
_closeDrawer();
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showSuppliers() {
|
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
|
||||||
_closeDrawer();
|
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showManufacturers() {
|
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
|
||||||
_closeDrawer();
|
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showCustomers() {
|
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
|
||||||
_closeDrawer();
|
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Load settings widget
|
* Load settings widget
|
||||||
*/
|
*/
|
||||||
@ -107,17 +65,14 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: ListTile.divideTiles(
|
children: ListTile.divideTiles(
|
||||||
context: context,
|
context: context,
|
||||||
tiles: <Widget>[
|
tiles: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Image.asset(
|
leading: FaIcon(FontAwesomeIcons.home),
|
||||||
"assets/image/icon.png",
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
width: 30,
|
|
||||||
),
|
|
||||||
title: Text(
|
title: Text(
|
||||||
L10().appTitle,
|
L10().appTitle,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
@ -134,35 +89,6 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
leading: FaIcon(FontAwesomeIcons.search),
|
leading: FaIcon(FontAwesomeIcons.search),
|
||||||
onTap: _search,
|
onTap: _search,
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
title: Text(L10().parts),
|
|
||||||
leading: Icon(Icons.category),
|
|
||||||
onTap: _showParts,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text(L10().stock),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.boxes),
|
|
||||||
onTap: _showStock,
|
|
||||||
),
|
|
||||||
|
|
||||||
/*
|
|
||||||
ListTile(
|
|
||||||
title: Text("Suppliers"),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.building),
|
|
||||||
onTap: _showSuppliers,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text("Manufacturers"),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.industry),
|
|
||||||
onTap: _showManufacturers,
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text("Customers"),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.users),
|
|
||||||
onTap: _showCustomers,
|
|
||||||
),
|
|
||||||
*/
|
|
||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().settings),
|
title: Text(L10().settings),
|
||||||
leading: Icon(Icons.settings),
|
leading: Icon(Icons.settings),
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
|
import "dart:async";
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import "package:file_picker/file_picker.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:image_picker/image_picker.dart';
|
import "package:image_picker/image_picker.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:one_context/one_context.dart";
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:one_context/one_context.dart';
|
|
||||||
|
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
|
|
||||||
class FilePickerDialog {
|
class FilePickerDialog {
|
||||||
@ -167,7 +165,7 @@ class CheckBoxField extends FormField<bool> {
|
|||||||
|
|
||||||
class StringField extends TextFormField {
|
class StringField extends TextFormField {
|
||||||
|
|
||||||
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) :
|
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function(String?)? validator, bool allowEmpty = false, bool isEnabled = true}) :
|
||||||
super(
|
super(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: allowEmpty ? label : label + "*",
|
labelText: allowEmpty ? label : label + "*",
|
||||||
@ -182,7 +180,7 @@ class StringField extends TextFormField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (validator != null) {
|
if (validator != null) {
|
||||||
return validator(value);
|
return validator(value) as String?;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -196,7 +194,7 @@ class StringField extends TextFormField {
|
|||||||
*/
|
*/
|
||||||
class QuantityField extends TextFormField {
|
class QuantityField extends TextFormField {
|
||||||
|
|
||||||
QuantityField({String label = "", String hint = "", String initial = "", double? max, TextEditingController? controller}) :
|
QuantityField({String label = "", String hint = "", double? max, TextEditingController? controller}) :
|
||||||
super(
|
super(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
import 'package:inventree/app_colors.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:inventree/user_profile.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
|
import "package:inventree/settings/settings.dart";
|
||||||
|
import "package:inventree/user_profile.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/barcode.dart";
|
||||||
|
import "package:inventree/api.dart";
|
||||||
|
import "package:inventree/settings/login.dart";
|
||||||
|
import "package:inventree/widget/category_display.dart";
|
||||||
|
import "package:inventree/widget/company_list.dart";
|
||||||
|
import "package:inventree/widget/location_display.dart";
|
||||||
|
import "package:inventree/widget/part_list.dart";
|
||||||
|
import "package:inventree/widget/purchase_order_list.dart";
|
||||||
|
import "package:inventree/widget/search.dart";
|
||||||
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
import "package:inventree/widget/drawer.dart";
|
||||||
|
|
||||||
import 'package:inventree/barcode.dart';
|
|
||||||
import 'package:inventree/api.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/settings/login.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/widget/category_display.dart';
|
|
||||||
import 'package:inventree/widget/company_list.dart';
|
|
||||||
import 'package:inventree/widget/location_display.dart';
|
|
||||||
import 'package:inventree/widget/search.dart';
|
|
||||||
import 'package:inventree/widget/spinner.dart';
|
|
||||||
import 'package:inventree/widget/drawer.dart';
|
|
||||||
|
|
||||||
class InvenTreeHomePage extends StatefulWidget {
|
class InvenTreeHomePage extends StatefulWidget {
|
||||||
|
|
||||||
InvenTreeHomePage({Key? key}) : super(key: key);
|
const InvenTreeHomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_InvenTreeHomePageState createState() => _InvenTreeHomePageState();
|
_InvenTreeHomePageState createState() => _InvenTreeHomePageState();
|
||||||
@ -29,33 +30,27 @@ class InvenTreeHomePage extends StatefulWidget {
|
|||||||
|
|
||||||
class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||||
|
|
||||||
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
|
|
||||||
|
|
||||||
_InvenTreeHomePageState() : super() {
|
_InvenTreeHomePageState() : super() {
|
||||||
|
|
||||||
// Initially load the profile and attempt server connection
|
// Initially load the profile and attempt server connection
|
||||||
_loadProfile();
|
_loadProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
|
||||||
|
|
||||||
// Selected user profile
|
// Selected user profile
|
||||||
UserProfile? _profile;
|
UserProfile? _profile;
|
||||||
|
|
||||||
void _searchParts() {
|
void _search(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
showSearch(
|
Navigator.push(
|
||||||
context: context,
|
context,
|
||||||
delegate: PartSearchDelegate(context)
|
MaterialPageRoute(
|
||||||
|
builder: (context) => SearchWidget()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
void _searchStock() {
|
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
|
||||||
|
|
||||||
showSearch(
|
|
||||||
context: context,
|
|
||||||
delegate: StockSearchDelegate(context)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scan(BuildContext context) {
|
void _scan(BuildContext context) {
|
||||||
@ -64,31 +59,60 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
scanQrCode(context);
|
scanQrCode(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parts(BuildContext context) {
|
void _showParts(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stock(BuildContext context) {
|
void _showSettings(BuildContext context) {
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showStarredParts(BuildContext context) {
|
||||||
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PartList({
|
||||||
|
"starred": "true"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showStock(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _suppliers() {
|
void _showPurchaseOrders(BuildContext context) {
|
||||||
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PurchaseOrderListWidget(filters: {})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void _showSuppliers(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _manufacturers() {
|
void _showManufacturers(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _customers() {
|
void _showCustomers(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
|
||||||
@ -103,7 +127,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadProfile() async {
|
Future <void> _loadProfile() async {
|
||||||
|
|
||||||
_profile = await UserProfileDBManager().getSelectedProfile();
|
_profile = await UserProfileDBManager().getSelectedProfile();
|
||||||
|
|
||||||
@ -121,270 +145,181 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
ListTile _serverTile() {
|
|
||||||
|
|
||||||
// No profile selected
|
Widget _iconButton(BuildContext context, String label, IconData icon, {Function()? callback, String role = "", String permission = ""}) {
|
||||||
// Tap to select / create a profile
|
|
||||||
if (_profile == null) {
|
bool connected = InvenTreeAPI().isConnected();
|
||||||
return ListTile(
|
|
||||||
title: Text(L10().profileNotSelected),
|
bool allowed = true;
|
||||||
subtitle: Text(L10().profileTapToCreate),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.server),
|
if (role.isNotEmpty || permission.isNotEmpty) {
|
||||||
trailing: FaIcon(
|
allowed = InvenTreeAPI().checkPermission(role, permission);
|
||||||
FontAwesomeIcons.user,
|
}
|
||||||
color: COLOR_DANGER,
|
|
||||||
|
return GestureDetector(
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
horizontal: 10
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
FaIcon(
|
||||||
|
icon,
|
||||||
|
color: connected && allowed ? COLOR_CLICK : Colors.grey,
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 12,
|
||||||
|
thickness: 0,
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_selectProfile();
|
|
||||||
|
if (!allowed) {
|
||||||
|
showSnackIcon(
|
||||||
|
L10().permissionRequired,
|
||||||
|
icon: FontAwesomeIcons.exclamationCircle,
|
||||||
|
success: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profile is selected ...
|
List<Widget> getGridTiles(BuildContext context) {
|
||||||
if (InvenTreeAPI().isConnecting()) {
|
return [
|
||||||
return ListTile(
|
_iconButton(
|
||||||
title: Text(L10().serverConnecting),
|
context,
|
||||||
subtitle: Text("${InvenTreeAPI().baseUrl}"),
|
L10().scanBarcode,
|
||||||
leading: FaIcon(FontAwesomeIcons.server),
|
FontAwesomeIcons.barcode,
|
||||||
trailing: Spinner(
|
callback: () {
|
||||||
icon: FontAwesomeIcons.spinner,
|
_scan(context);
|
||||||
color: COLOR_PROGRESS,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_selectProfile();
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} else if (InvenTreeAPI().isConnected()) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(L10().serverConnected),
|
|
||||||
subtitle: Text("${InvenTreeAPI().baseUrl}"),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.server),
|
|
||||||
trailing: FaIcon(
|
|
||||||
FontAwesomeIcons.checkCircle,
|
|
||||||
color: COLOR_SUCCESS
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
_iconButton(
|
||||||
_selectProfile();
|
context,
|
||||||
},
|
L10().search,
|
||||||
);
|
FontAwesomeIcons.search,
|
||||||
} else {
|
callback: () {
|
||||||
return ListTile(
|
_search(context);
|
||||||
title: Text(L10().serverCouldNotConnect),
|
|
||||||
subtitle: Text("${_profile!.server}"),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.server),
|
|
||||||
trailing: FaIcon(
|
|
||||||
FontAwesomeIcons.timesCircle,
|
|
||||||
color: COLOR_DANGER,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_selectProfile();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
),
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().parts,
|
||||||
|
FontAwesomeIcons.shapes,
|
||||||
|
callback: () {
|
||||||
|
_showParts(context);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().partsStarred,
|
||||||
|
FontAwesomeIcons.solidStar,
|
||||||
|
callback: () {
|
||||||
|
_showStarredParts(context);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().stock,
|
||||||
|
FontAwesomeIcons.boxes,
|
||||||
|
callback: () {
|
||||||
|
_showStock(context);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().purchaseOrders,
|
||||||
|
FontAwesomeIcons.shoppingCart,
|
||||||
|
callback: () {
|
||||||
|
_showPurchaseOrders(context);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
/*
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().salesOrders,
|
||||||
|
FontAwesomeIcons.truck,
|
||||||
|
),
|
||||||
|
*/
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().suppliers,
|
||||||
|
FontAwesomeIcons.building,
|
||||||
|
callback: () {
|
||||||
|
_showSuppliers(context);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().manufacturers,
|
||||||
|
FontAwesomeIcons.industry,
|
||||||
|
callback: () {
|
||||||
|
_showManufacturers(context);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().customers,
|
||||||
|
FontAwesomeIcons.userTie,
|
||||||
|
callback: () {
|
||||||
|
_showCustomers(context);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_iconButton(
|
||||||
|
context,
|
||||||
|
L10().settings,
|
||||||
|
FontAwesomeIcons.cogs,
|
||||||
|
callback: () {
|
||||||
|
_showSettings(context);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
// This method is rerun every time setState is called, for instance as done
|
|
||||||
// by the _incrementCounter method above.
|
|
||||||
//
|
|
||||||
// The Flutter framework has been optimized to make rerunning build methods
|
|
||||||
// fast, so that you can just rebuild anything that needs updating rather
|
|
||||||
// than having to individually change instances of widgets.
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _homeKey,
|
key: _homeKey,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(L10().appTitle),
|
title: Text(L10().appTitle),
|
||||||
actions: <Widget>[
|
actions: [
|
||||||
/*
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.search),
|
icon: FaIcon(
|
||||||
tooltip: L10().search,
|
FontAwesomeIcons.server,
|
||||||
onPressed: _searchParts,
|
color: InvenTreeAPI().isConnected() ? COLOR_SUCCESS : COLOR_DANGER,
|
||||||
),
|
),
|
||||||
*/
|
onPressed: _selectProfile,
|
||||||
],
|
|
||||||
),
|
|
||||||
drawer: new InvenTreeDrawer(context),
|
|
||||||
body: Center(
|
|
||||||
// Center is a layout widget. It takes a single child and positions it
|
|
||||||
// in the middle of the parent.
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: (<Widget>[
|
|
||||||
Spacer(),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: new FaIcon(FontAwesomeIcons.barcode),
|
|
||||||
tooltip: L10().scanBarcode,
|
|
||||||
onPressed: () { _scan(context); },
|
|
||||||
),
|
|
||||||
Text(L10().scanBarcode),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: new FaIcon(FontAwesomeIcons.shapes),
|
|
||||||
tooltip: L10().parts,
|
|
||||||
onPressed: () { _parts(context); },
|
|
||||||
),
|
|
||||||
Text(L10().parts),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
|
|
||||||
IconButton(
|
|
||||||
icon: new FaIcon(FontAwesomeIcons.search),
|
|
||||||
tooltip: L10().searchParts,
|
|
||||||
onPressed: _searchParts,
|
|
||||||
),
|
|
||||||
Text(L10().searchParts),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// TODO - Re-add starred parts link
|
|
||||||
/*
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(FontAwesomeIcons.solidStar),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => StarredPartWidget()));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text("Starred Parts"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
*/
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: new FaIcon(FontAwesomeIcons.boxes),
|
|
||||||
tooltip: L10().stock,
|
|
||||||
onPressed: () { _stock(context); },
|
|
||||||
),
|
|
||||||
Text(L10().stock),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: new FaIcon(FontAwesomeIcons.search),
|
|
||||||
tooltip: L10().searchStock,
|
|
||||||
onPressed: _searchStock,
|
|
||||||
),
|
|
||||||
Text(L10().searchStock),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
// TODO - Re-add these when the features actually do something..
|
|
||||||
/*
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: new FaIcon(FontAwesomeIcons.building),
|
|
||||||
tooltip: "Suppliers",
|
|
||||||
onPressed: _suppliers,
|
|
||||||
),
|
|
||||||
Text("Suppliers"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(FontAwesomeIcons.industry),
|
|
||||||
tooltip: "Manufacturers",
|
|
||||||
onPressed: _manufacturers,
|
|
||||||
),
|
|
||||||
Text("Manufacturers")
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(FontAwesomeIcons.userTie),
|
|
||||||
tooltip: "Customers",
|
|
||||||
onPressed: _customers,
|
|
||||||
),
|
|
||||||
Text("Customers"),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Spacer(),
|
drawer: InvenTreeDrawer(context),
|
||||||
Row(
|
body: ListView(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
children: [
|
||||||
children: <Widget>[
|
GridView.extent(
|
||||||
Column(
|
maxCrossAxisExtent: 140,
|
||||||
children: <Widget>[
|
shrinkWrap: true,
|
||||||
IconButton(
|
physics: ClampingScrollPhysics(),
|
||||||
icon: new FaIcon(FontAwesomeIcons.tools),
|
children: getGridTiles(context),
|
||||||
tooltip: "Build",
|
|
||||||
onPressed: _unsupported,
|
|
||||||
),
|
|
||||||
Text("Build"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: new FaIcon(FontAwesomeIcons.shoppingCart),
|
|
||||||
tooltip: "Order",
|
|
||||||
onPressed: _unsupported,
|
|
||||||
),
|
|
||||||
Text("Order"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: new FaIcon(FontAwesomeIcons.truck),
|
|
||||||
tooltip: "Ship",
|
|
||||||
onPressed: _unsupported,
|
|
||||||
),
|
|
||||||
Text("Ship"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
*/
|
|
||||||
Spacer(),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: _serverTile(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
import 'package:inventree/api.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:inventree/app_colors.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:inventree/app_settings.dart';
|
import "package:flutter/foundation.dart";
|
||||||
import 'package:inventree/barcode.dart';
|
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
|
||||||
import 'package:inventree/inventree/stock.dart';
|
|
||||||
import 'package:inventree/widget/progress.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/widget/stock_detail.dart';
|
|
||||||
import 'package:inventree/widget/paginator.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
|
import "package:inventree/barcode.dart";
|
||||||
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
import "package:inventree/widget/progress.dart";
|
||||||
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
import "package:inventree/widget/stock_detail.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/widget/stock_list.dart";
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
|
|
||||||
class LocationDisplayWidget extends StatefulWidget {
|
class LocationDisplayWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -31,6 +29,8 @@ class LocationDisplayWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||||
|
|
||||||
|
_LocationDisplayState(this.location);
|
||||||
|
|
||||||
final InvenTreeStockLocation? location;
|
final InvenTreeStockLocation? location;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -62,7 +62,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ((location != null) && (InvenTreeAPI().checkPermission('stock_location', 'change'))) {
|
if ((location != null) && (InvenTreeAPI().checkPermission("stock_location", "change"))) {
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
@ -92,11 +92,9 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_LocationDisplayState(this.location);
|
|
||||||
|
|
||||||
List<InvenTreeStockLocation> _sublocations = [];
|
List<InvenTreeStockLocation> _sublocations = [];
|
||||||
|
|
||||||
String _locationFilter = '';
|
String _locationFilter = "";
|
||||||
|
|
||||||
List<InvenTreeStockLocation> get sublocations {
|
List<InvenTreeStockLocation> get sublocations {
|
||||||
|
|
||||||
@ -146,7 +144,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
data: {
|
data: {
|
||||||
"parent": (pk > 0) ? pk : null,
|
"parent": (pk > 0) ? pk : null,
|
||||||
},
|
},
|
||||||
onSuccess: (data) async {
|
onSuccess: (result) async {
|
||||||
|
|
||||||
|
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||||
|
|
||||||
if (data.containsKey("pk")) {
|
if (data.containsKey("pk")) {
|
||||||
var loc = InvenTreeStockLocation.fromJson(data);
|
var loc = InvenTreeStockLocation.fromJson(data);
|
||||||
|
|
||||||
@ -175,7 +176,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
data: {
|
data: {
|
||||||
"location": pk,
|
"location": pk,
|
||||||
},
|
},
|
||||||
onSuccess: (data) async {
|
onSuccess: (result) async {
|
||||||
|
|
||||||
|
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||||
|
|
||||||
if (data.containsKey("pk")) {
|
if (data.containsKey("pk")) {
|
||||||
var item = InvenTreeStockItem.fromJson(data);
|
var item = InvenTreeStockItem.fromJson(data);
|
||||||
|
|
||||||
@ -280,7 +284,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
children: detailTiles(),
|
children: detailTiles(),
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return PaginatedStockList(filters);
|
return PaginatedStockItemList(filters);
|
||||||
case 2:
|
case 2:
|
||||||
return ListView(
|
return ListView(
|
||||||
children: ListTile.divideTiles(
|
children: ListTile.divideTiles(
|
||||||
@ -307,13 +311,13 @@ List<Widget> detailTiles() {
|
|||||||
L10().sublocations,
|
L10().sublocations,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
trailing: sublocations.length > 0 ? Text("${sublocations.length}") : null,
|
trailing: sublocations.isNotEmpty ? Text("${sublocations.length}") : null,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
tiles.add(progressIndicator());
|
tiles.add(progressIndicator());
|
||||||
} else if (_sublocations.length > 0) {
|
} else if (_sublocations.isNotEmpty) {
|
||||||
tiles.add(SublocationList(_sublocations));
|
tiles.add(SublocationList(_sublocations));
|
||||||
} else {
|
} else {
|
||||||
tiles.add(ListTile(
|
tiles.add(ListTile(
|
||||||
@ -334,7 +338,7 @@ List<Widget> detailTiles() {
|
|||||||
|
|
||||||
tiles.add(locationDescriptionCard(includeActions: false));
|
tiles.add(locationDescriptionCard(includeActions: false));
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('stock', 'add')) {
|
if (InvenTreeAPI().checkPermission("stock", "add")) {
|
||||||
|
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -362,7 +366,7 @@ List<Widget> detailTiles() {
|
|||||||
|
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
// Stock adjustment actions
|
// Stock adjustment actions
|
||||||
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
||||||
// Scan items into location
|
// Scan items into location
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -422,9 +426,10 @@ List<Widget> detailTiles() {
|
|||||||
|
|
||||||
|
|
||||||
class SublocationList extends StatelessWidget {
|
class SublocationList extends StatelessWidget {
|
||||||
final List<InvenTreeStockLocation> _locations;
|
|
||||||
|
|
||||||
SublocationList(this._locations);
|
const SublocationList(this._locations);
|
||||||
|
|
||||||
|
final List<InvenTreeStockLocation> _locations;
|
||||||
|
|
||||||
void _openLocation(BuildContext context, int pk) {
|
void _openLocation(BuildContext context, int pk) {
|
||||||
|
|
||||||
@ -440,7 +445,7 @@ class SublocationList extends StatelessWidget {
|
|||||||
InvenTreeStockLocation loc = _locations[index];
|
InvenTreeStockLocation loc = _locations[index];
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text('${loc.name}'),
|
title: Text("${loc.name}"),
|
||||||
subtitle: Text("${loc.description}"),
|
subtitle: Text("${loc.description}"),
|
||||||
trailing: Text("${loc.itemcount}"),
|
trailing: Text("${loc.itemcount}"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -460,162 +465,3 @@ class SublocationList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Widget for displaying a list of stock items within a stock location.
|
|
||||||
*
|
|
||||||
* Users server-side pagination for snappy results
|
|
||||||
*/
|
|
||||||
|
|
||||||
class PaginatedStockList extends StatefulWidget {
|
|
||||||
|
|
||||||
final Map<String, String> filters;
|
|
||||||
|
|
||||||
PaginatedStockList(this.filters);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_PaginatedStockListState createState() => _PaginatedStockListState(filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class _PaginatedStockListState extends State<PaginatedStockList> {
|
|
||||||
|
|
||||||
static const _pageSize = 25;
|
|
||||||
|
|
||||||
String _searchTerm = "";
|
|
||||||
|
|
||||||
final Map<String, String> filters;
|
|
||||||
|
|
||||||
_PaginatedStockListState(this.filters);
|
|
||||||
|
|
||||||
final PagingController<int, InvenTreeStockItem> _pagingController = PagingController(firstPageKey: 0);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_pagingController.addPageRequestListener((pageKey) {
|
|
||||||
_fetchPage(pageKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
int resultCount = 0;
|
|
||||||
|
|
||||||
Future<void> _fetchPage(int pageKey) async {
|
|
||||||
try {
|
|
||||||
|
|
||||||
Map<String, String> params = this.filters;
|
|
||||||
|
|
||||||
params["search"] = "${_searchTerm}";
|
|
||||||
|
|
||||||
// Do we include stock items from sub-locations?
|
|
||||||
final bool cascade = await InvenTreeSettingsManager().getValue("stockSublocation", true);
|
|
||||||
params["cascade"] = "${cascade}";
|
|
||||||
|
|
||||||
final page = await InvenTreeStockItem().listPaginated(_pageSize, pageKey, filters: params);
|
|
||||||
|
|
||||||
int pageLength = page?.length ?? 0;
|
|
||||||
int pageCount = page?.count ?? 0;
|
|
||||||
|
|
||||||
final isLastPage = pageLength < _pageSize;
|
|
||||||
|
|
||||||
// Construct a list of stock item objects
|
|
||||||
List<InvenTreeStockItem> items = [];
|
|
||||||
|
|
||||||
if (page != null) {
|
|
||||||
for (var result in page.results) {
|
|
||||||
if (result is InvenTreeStockItem) {
|
|
||||||
items.add(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLastPage) {
|
|
||||||
_pagingController.appendLastPage(items);
|
|
||||||
} else {
|
|
||||||
final int nextPageKey = pageKey + pageLength;
|
|
||||||
_pagingController.appendPage(items, nextPageKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
resultCount = pageCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
_pagingController.error = error;
|
|
||||||
|
|
||||||
sentryReportError(error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openItem(BuildContext context, int pk) {
|
|
||||||
InvenTreeStockItem().get(pk).then((var item) {
|
|
||||||
if (item is InvenTreeStockItem) {
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildItem(BuildContext context, InvenTreeStockItem item) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text("${item.partName}"),
|
|
||||||
subtitle: Text("${item.locationPathString}"),
|
|
||||||
leading: InvenTreeAPI().getImage(
|
|
||||||
item.partThumbnail,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
trailing: Text("${item.displayQuantity}",
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_openItem(context, item.pk);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final TextEditingController searchController = TextEditingController();
|
|
||||||
|
|
||||||
void updateSearchTerm() {
|
|
||||||
_searchTerm = searchController.text;
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build (BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
|
|
||||||
Expanded(
|
|
||||||
child: CustomScrollView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: ClampingScrollPhysics(),
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
slivers: <Widget>[
|
|
||||||
// TODO - Search input
|
|
||||||
PagedSliverList.separated(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate<InvenTreeStockItem>(
|
|
||||||
itemBuilder: (context, item, index) {
|
|
||||||
return _buildItem(context, item);
|
|
||||||
},
|
|
||||||
noItemsFoundIndicatorBuilder: (context) {
|
|
||||||
return NoResultsWidget("No stock items found");
|
|
||||||
}
|
|
||||||
),
|
|
||||||
separatorBuilder: (context, item) => const Divider(height: 1),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
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:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:infinite_scroll_pagination/infinite_scroll_pagination.dart";
|
||||||
import 'package:inventree/l10.dart';
|
|
||||||
|
import "package:inventree/inventree/model.dart";
|
||||||
|
import "package:inventree/inventree/sentry.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
|
|
||||||
|
class PaginatedSearchState<T extends StatefulWidget> extends State<T> {
|
||||||
|
|
||||||
|
PaginatedSearchState(this.filters);
|
||||||
|
|
||||||
|
final Map<String, String> filters;
|
||||||
|
|
||||||
|
static const _pageSize = 25;
|
||||||
|
|
||||||
|
// Search query term
|
||||||
|
String searchTerm = "";
|
||||||
|
|
||||||
|
int resultCount = 0;
|
||||||
|
|
||||||
|
// Text controller
|
||||||
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
|
||||||
|
// Pagination controller
|
||||||
|
final PagingController<int, InvenTreeModel> _pagingController = PagingController(firstPageKey: 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_pagingController.addPageRequestListener((pageKey) {
|
||||||
|
_fetchPage(pageKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pagingController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
||||||
|
|
||||||
|
print("Blank request page");
|
||||||
|
// Default implementation returns null - must be overridden
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchPage(int pageKey) async {
|
||||||
|
try {
|
||||||
|
Map<String, String> params = filters;
|
||||||
|
|
||||||
|
params["search"] = "${searchTerm}";
|
||||||
|
|
||||||
|
final page = await requestPage(
|
||||||
|
_pageSize,
|
||||||
|
pageKey,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
int pageLength = page?.length ?? 0;
|
||||||
|
int pageCount = page?.count ?? 0;
|
||||||
|
|
||||||
|
final isLastPage = pageLength < _pageSize;
|
||||||
|
|
||||||
|
List<InvenTreeModel> items = [];
|
||||||
|
|
||||||
|
if (page != null) {
|
||||||
|
for (var result in page.results) {
|
||||||
|
if (result is InvenTreeModel) {
|
||||||
|
items.add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) {
|
||||||
|
_pagingController.appendLastPage(items);
|
||||||
|
} else {
|
||||||
|
final int nextPageKey = pageKey + pageLength;
|
||||||
|
_pagingController.appendPage(items, nextPageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
resultCount = pageCount;
|
||||||
|
});
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_pagingController.error = error;
|
||||||
|
|
||||||
|
sentryReportError(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchTerm() {
|
||||||
|
searchTerm = searchController.text;
|
||||||
|
_pagingController.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildItem(BuildContext context, InvenTreeModel item) {
|
||||||
|
|
||||||
|
// This method must be overridden by the child class
|
||||||
|
return ListTile(
|
||||||
|
title: Text("*** UNIMPLEMENTED ***"),
|
||||||
|
subtitle: Text("*** buildItem() is unimplemented for this widget!"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get noResultsText => L10().noResults;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build (BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
PaginatedSearchWidget(searchController, updateSearchTerm, resultCount),
|
||||||
|
Expanded(
|
||||||
|
child: CustomScrollView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: ClampingScrollPhysics(),
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
slivers: <Widget>[
|
||||||
|
// TODO - Search input
|
||||||
|
PagedSliverList.separated(
|
||||||
|
pagingController: _pagingController,
|
||||||
|
builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>(
|
||||||
|
itemBuilder: (context, item, index) {
|
||||||
|
return buildItem(context, item);
|
||||||
|
},
|
||||||
|
noItemsFoundIndicatorBuilder: (context) {
|
||||||
|
return NoResultsWidget(noResultsText);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
separatorBuilder: (context, item) => const Divider(height: 1),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PaginatedSearchWidget extends StatelessWidget {
|
class PaginatedSearchWidget extends StatelessWidget {
|
||||||
|
|
||||||
Function onChanged;
|
const PaginatedSearchWidget(this.controller, this.onChanged, this.results);
|
||||||
|
|
||||||
int results = 0;
|
final Function onChanged;
|
||||||
|
|
||||||
TextEditingController controller;
|
final int results;
|
||||||
|
|
||||||
PaginatedSearchWidget(this.controller, this.onChanged, this.results);
|
final TextEditingController controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -44,9 +183,9 @@ class PaginatedSearchWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class NoResultsWidget extends StatelessWidget {
|
class NoResultsWidget extends StatelessWidget {
|
||||||
|
|
||||||
final String description;
|
const NoResultsWidget(this.description);
|
||||||
|
|
||||||
NoResultsWidget(this.description);
|
final String description;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:flutter/cupertino.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:inventree/widget/fields.dart";
|
||||||
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:inventree/l10.dart";
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:inventree/inventree/part.dart';
|
|
||||||
import 'package:inventree/widget/fields.dart';
|
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
|
||||||
import 'package:inventree/widget/snacks.dart';
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import '../api.dart';
|
|
||||||
import '../l10.dart';
|
|
||||||
|
|
||||||
class PartAttachmentsWidget extends StatefulWidget {
|
class PartAttachmentsWidget extends StatefulWidget {
|
||||||
|
|
||||||
PartAttachmentsWidget(this.part, {Key? key}) : super(key: key);
|
const PartAttachmentsWidget(this.part, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
final InvenTreePart part;
|
final InvenTreePart part;
|
||||||
|
|
||||||
@ -42,7 +38,7 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
|
|||||||
|
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('part', 'change')) {
|
if (InvenTreeAPI().checkPermission("part", "change")) {
|
||||||
|
|
||||||
// File upload
|
// File upload
|
||||||
actions.add(
|
actions.add(
|
||||||
@ -127,7 +123,7 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tiles.length == 0) {
|
if (tiles.isEmpty) {
|
||||||
tiles.add(ListTile(
|
tiles.add(ListTile(
|
||||||
title: Text(L10().attachmentNone),
|
title: Text(L10().attachmentNone),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/foundation.dart';
|
import "package:flutter/foundation.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:inventree/app_colors.dart';
|
|
||||||
import 'package:inventree/inventree/stock.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/widget/part_attachments_widget.dart';
|
|
||||||
import 'package:inventree/widget/part_notes.dart';
|
|
||||||
import 'package:inventree/widget/progress.dart';
|
|
||||||
import 'package:inventree/inventree/part.dart';
|
|
||||||
import 'package:inventree/widget/category_display.dart';
|
|
||||||
import 'package:inventree/api.dart';
|
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
|
||||||
import 'package:inventree/widget/part_image_widget.dart';
|
|
||||||
import 'package:inventree/widget/stock_detail.dart';
|
|
||||||
|
|
||||||
import 'location_display.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/widget/part_attachments_widget.dart";
|
||||||
|
import "package:inventree/widget/part_notes.dart";
|
||||||
|
import "package:inventree/widget/progress.dart";
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:inventree/widget/category_display.dart";
|
||||||
|
import "package:inventree/api.dart";
|
||||||
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
import "package:inventree/widget/part_image_widget.dart";
|
||||||
|
import "package:inventree/widget/stock_detail.dart";
|
||||||
|
import "package:inventree/widget/stock_list.dart";
|
||||||
|
|
||||||
|
|
||||||
class PartDetailWidget extends StatefulWidget {
|
class PartDetailWidget extends StatefulWidget {
|
||||||
|
|
||||||
PartDetailWidget(this.part, {Key? key}) : super(key: key);
|
const PartDetailWidget(this.part, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
final InvenTreePart part;
|
final InvenTreePart part;
|
||||||
|
|
||||||
@ -34,10 +34,10 @@ class PartDetailWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||||
|
|
||||||
InvenTreePart part;
|
|
||||||
|
|
||||||
_PartDisplayState(this.part);
|
_PartDisplayState(this.part);
|
||||||
|
|
||||||
|
InvenTreePart part;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().partDetails;
|
String getAppBarTitle(BuildContext context) => L10().partDetails;
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
|
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('part', 'view')) {
|
if (InvenTreeAPI().checkPermission("part", "view")) {
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.globe),
|
icon: FaIcon(FontAwesomeIcons.globe),
|
||||||
@ -55,7 +55,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('part', 'change')) {
|
if (InvenTreeAPI().checkPermission("part", "change")) {
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
@ -89,9 +89,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
await part.getTestTemplates();
|
await part.getTestTemplates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleStar() async {
|
Future <void> _toggleStar() async {
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('part', 'view')) {
|
if (InvenTreeAPI().checkPermission("part", "view")) {
|
||||||
await part.update(values: {"starred": "${!part.starred}"});
|
await part.update(values: {"starred": "${!part.starred}"});
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
@ -327,7 +327,8 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO - Add request tests?
|
// TODO - Add request tests?
|
||||||
if (false && part.isTrackable) {
|
/*
|
||||||
|
if (part.isTrackable) {
|
||||||
tiles.add(ListTile(
|
tiles.add(ListTile(
|
||||||
title: Text(L10().testsRequired),
|
title: Text(L10().testsRequired),
|
||||||
leading: FaIcon(FontAwesomeIcons.tasks),
|
leading: FaIcon(FontAwesomeIcons.tasks),
|
||||||
@ -336,6 +337,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Notes field
|
// Notes field
|
||||||
tiles.add(
|
tiles.add(
|
||||||
@ -398,6 +400,12 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
|
|
||||||
fields["part"]["hidden"] = true;
|
fields["part"]["hidden"] = true;
|
||||||
|
|
||||||
|
int? default_location = part.defaultLocation;
|
||||||
|
|
||||||
|
if (default_location != null) {
|
||||||
|
fields["location"]["value"] = default_location;
|
||||||
|
}
|
||||||
|
|
||||||
InvenTreeStockItem().createForm(
|
InvenTreeStockItem().createForm(
|
||||||
context,
|
context,
|
||||||
L10().stockItemCreate,
|
L10().stockItemCreate,
|
||||||
@ -405,7 +413,10 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
data: {
|
data: {
|
||||||
"part": "${part.pk}",
|
"part": "${part.pk}",
|
||||||
},
|
},
|
||||||
onSuccess: (data) async {
|
onSuccess: (result) async {
|
||||||
|
|
||||||
|
Map<String, dynamic> data = result as Map<String, dynamic>;
|
||||||
|
|
||||||
if (data.containsKey("pk")) {
|
if (data.containsKey("pk")) {
|
||||||
var item = InvenTreeStockItem.fromJson(data);
|
var item = InvenTreeStockItem.fromJson(data);
|
||||||
|
|
||||||
@ -437,7 +448,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// TODO - Add this action back in once implemented
|
// TODO - Add this action back in once implemented
|
||||||
if (false) {
|
/*
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().barcodeScanItem),
|
title: Text(L10().barcodeScanItem),
|
||||||
@ -448,9 +459,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
*/
|
||||||
|
|
||||||
if (false && !part.isActive && InvenTreeAPI().checkPermission('part', 'delete')) {
|
/*
|
||||||
|
// TODO: Implement part deletion
|
||||||
|
if (!part.isActive && InvenTreeAPI().checkPermission("part", "delete")) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().deletePart),
|
title: Text(L10().deletePart),
|
||||||
@ -462,6 +475,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
@ -480,7 +494,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return PaginatedStockList({"part": "${part.pk}"});
|
return PaginatedStockItemList(
|
||||||
|
{"part": "${part.pk}"}
|
||||||
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return Center(
|
return Center(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
import 'dart:io';
|
import "dart:io";
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/foundation.dart';
|
import "package:flutter/foundation.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:inventree/inventree/part.dart';
|
import "package:inventree/inventree/part.dart";
|
||||||
import 'package:inventree/widget/fields.dart';
|
import "package:inventree/widget/fields.dart";
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
import 'package:inventree/widget/snacks.dart';
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
import '../l10.dart';
|
|
||||||
|
|
||||||
class PartImageWidget extends StatefulWidget {
|
class PartImageWidget extends StatefulWidget {
|
||||||
|
|
||||||
PartImageWidget(this.part, {Key? key}) : super(key: key);
|
const PartImageWidget(this.part, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
final InvenTreePart part;
|
final InvenTreePart part;
|
||||||
|
|
||||||
@ -46,7 +44,7 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
|
|||||||
|
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('part', 'change')) {
|
if (InvenTreeAPI().checkPermission("part", "change")) {
|
||||||
|
|
||||||
// File upload
|
// File upload
|
||||||
actions.add(
|
actions.add(
|
||||||
|
100
lib/widget/part_list.dart
Normal file
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:flutter/material.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/api.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:inventree/inventree/part.dart';
|
import "package:inventree/inventree/part.dart";
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import "package:flutter_markdown/flutter_markdown.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
|
|
||||||
class PartNotesWidget extends StatefulWidget {
|
class PartNotesWidget extends StatefulWidget {
|
||||||
|
|
||||||
final InvenTreePart part;
|
const PartNotesWidget(this.part, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
PartNotesWidget(this.part, {Key? key}) : super(key: key);
|
final InvenTreePart part;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PartNotesState createState() => _PartNotesState(part);
|
_PartNotesState createState() => _PartNotesState(part);
|
||||||
@ -21,10 +21,10 @@ class PartNotesWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _PartNotesState extends RefreshableState<PartNotesWidget> {
|
class _PartNotesState extends RefreshableState<PartNotesWidget> {
|
||||||
|
|
||||||
final InvenTreePart part;
|
|
||||||
|
|
||||||
_PartNotesState(this.part);
|
_PartNotesState(this.part);
|
||||||
|
|
||||||
|
final InvenTreePart part;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> request() async {
|
Future<void> request() async {
|
||||||
await part.reload();
|
await part.reload();
|
||||||
@ -38,7 +38,7 @@ class _PartNotesState extends RefreshableState<PartNotesWidget> {
|
|||||||
|
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('part', 'change')) {
|
if (InvenTreeAPI().checkPermission("part", "change")) {
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import 'package:inventree/l10.dart';
|
import "dart:core";
|
||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
import 'dart:core';
|
import "package:inventree/api.dart";
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:inventree/inventree/part.dart';
|
import "package:inventree/inventree/part.dart";
|
||||||
import 'package:inventree/inventree/company.dart';
|
import "package:inventree/inventree/company.dart";
|
||||||
import 'package:inventree/widget/company_detail.dart';
|
import "package:inventree/widget/company_detail.dart";
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
|
||||||
class PartSupplierWidget extends StatefulWidget {
|
class PartSupplierWidget extends StatefulWidget {
|
||||||
|
|
||||||
PartSupplierWidget(this.part, {Key? key}) : super(key: key);
|
const PartSupplierWidget(this.part, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
final InvenTreePart part;
|
final InvenTreePart part;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct a circular progress indicator
|
* 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:inventree/widget/back.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:inventree/widget/drawer.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/widgets.dart';
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter/widgets.dart";
|
||||||
|
|
||||||
|
|
||||||
abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||||
@ -9,7 +10,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
|||||||
final refreshableKey = GlobalKey<ScaffoldState>();
|
final refreshableKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
// Storage for context once "Build" is called
|
// Storage for context once "Build" is called
|
||||||
BuildContext? _context;
|
late BuildContext? _context;
|
||||||
|
|
||||||
// Current tab index (used for widgets which display bottom tabs)
|
// Current tab index (used for widgets which display bottom tabs)
|
||||||
int tabIndex = 0;
|
int tabIndex = 0;
|
||||||
@ -32,6 +33,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
|||||||
|
|
||||||
String getAppBarTitle(BuildContext context) { return "App Bar Title"; }
|
String getAppBarTitle(BuildContext context) { return "App Bar Title"; }
|
||||||
|
|
||||||
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance?.addPostFrameCallback((_) => onBuild(_context!));
|
WidgetsBinding.instance?.addPostFrameCallback((_) => onBuild(_context!));
|
||||||
@ -60,14 +62,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to construct an appbar (override if needed)
|
|
||||||
AppBar getAppBar(BuildContext context) {
|
|
||||||
return AppBar(
|
|
||||||
title: Text(getAppBarTitle(context)),
|
|
||||||
actions: getAppBarActions(context),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to construct a drawer (override if needed)
|
// Function to construct a drawer (override if needed)
|
||||||
Widget getDrawer(BuildContext context) {
|
Widget getDrawer(BuildContext context) {
|
||||||
return InvenTreeDrawer(context);
|
return InvenTreeDrawer(context);
|
||||||
@ -96,8 +90,12 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: refreshableKey,
|
key: refreshableKey,
|
||||||
appBar: getAppBar(context),
|
appBar: AppBar(
|
||||||
drawer: null,
|
title: Text(getAppBarTitle(context)),
|
||||||
|
actions: getAppBarActions(context),
|
||||||
|
leading: backButton(context, refreshableKey),
|
||||||
|
),
|
||||||
|
drawer: getDrawer(context),
|
||||||
floatingActionButton: getFab(context),
|
floatingActionButton: getFab(context),
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
@ -1,393 +1,347 @@
|
|||||||
|
import "dart:async";
|
||||||
|
|
||||||
import 'package:inventree/widget/part_detail.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:inventree/widget/progress.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:inventree/widget/snacks.dart';
|
|
||||||
import 'package:inventree/widget/stock_detail.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:inventree/l10.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/inventree/part.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/inventree/stock.dart';
|
|
||||||
|
|
||||||
import '../api.dart';
|
import "package:inventree/inventree/company.dart";
|
||||||
|
import "package:inventree/inventree/purchase_order.dart";
|
||||||
|
import "package:inventree/widget/part_list.dart";
|
||||||
|
import "package:inventree/widget/purchase_order_list.dart";
|
||||||
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
import "package:inventree/widget/stock_list.dart";
|
||||||
|
import "package:inventree/widget/category_list.dart";
|
||||||
|
import "package:inventree/widget/company_list.dart";
|
||||||
|
import "package:inventree/widget/location_list.dart";
|
||||||
|
|
||||||
// TODO - Refactor duplicate code in this file!
|
|
||||||
|
|
||||||
class PartSearchDelegate extends SearchDelegate<InvenTreePart?> {
|
// Widget for performing database-wide search
|
||||||
|
class SearchWidget extends StatefulWidget {
|
||||||
final partSearchKey = GlobalKey<ScaffoldState>();
|
|
||||||
|
|
||||||
BuildContext context;
|
|
||||||
|
|
||||||
// What did we search for last time?
|
|
||||||
String _cachedQuery = "";
|
|
||||||
|
|
||||||
bool _searching = false;
|
|
||||||
|
|
||||||
// Custom filters for the part search
|
|
||||||
Map<String, String> _filters = {};
|
|
||||||
|
|
||||||
PartSearchDelegate(this.context, {Map<String, String> filters = const {}}) {
|
|
||||||
|
|
||||||
// Copy filter values
|
|
||||||
for (String key in filters.keys) {
|
|
||||||
|
|
||||||
String? value = filters[key];
|
|
||||||
|
|
||||||
if (value != null) {
|
|
||||||
_filters[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get searchFieldLabel => L10().searchParts;
|
_SearchDisplayState createState() => _SearchDisplayState();
|
||||||
|
|
||||||
// List of part results
|
|
||||||
List<InvenTreePart> partResults = [];
|
|
||||||
|
|
||||||
Future<void> search(BuildContext context) async {
|
|
||||||
|
|
||||||
// Search string too short!
|
|
||||||
if (query.length < 3) {
|
|
||||||
partResults.clear();
|
|
||||||
showResults(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query == _cachedQuery) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_cachedQuery = query;
|
|
||||||
|
|
||||||
_searching = true;
|
|
||||||
|
|
||||||
print("Searching...");
|
|
||||||
|
|
||||||
showResults(context);
|
|
||||||
|
|
||||||
_filters["cascade"] = "true";
|
|
||||||
|
|
||||||
final results = await InvenTreePart().search(context, query, filters: _filters);
|
|
||||||
|
|
||||||
partResults.clear();
|
|
||||||
|
|
||||||
for (int idx = 0; idx < results.length; idx++) {
|
|
||||||
if (results[idx] is InvenTreePart) {
|
|
||||||
partResults.add(results[idx] as InvenTreePart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Searching complete! Results: ${partResults.length}");
|
|
||||||
_searching = false;
|
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
"${partResults.length} ${L10().results}",
|
|
||||||
success: partResults.length > 0,
|
|
||||||
icon: FontAwesomeIcons.pollH,
|
|
||||||
);
|
|
||||||
|
|
||||||
// For some reason, need to toggle between suggestions and results here...
|
|
||||||
showSuggestions(context);
|
|
||||||
showResults(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildActions(BuildContext context) {
|
|
||||||
return [
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(FontAwesomeIcons.backspace),
|
|
||||||
onPressed: () {
|
|
||||||
query = '';
|
|
||||||
search(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(FontAwesomeIcons.search),
|
|
||||||
onPressed: () {
|
|
||||||
search(context);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildLeading(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
icon: Icon(Icons.arrow_back),
|
|
||||||
onPressed: () {
|
|
||||||
this.close(context, null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _partResult(BuildContext context, int index) {
|
|
||||||
|
|
||||||
InvenTreePart part = partResults[index];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
title: Text(part.fullname),
|
|
||||||
subtitle: Text(part.description),
|
|
||||||
leading: InvenTreeAPI().getImage(
|
|
||||||
part.thumbnail,
|
|
||||||
width: 40,
|
|
||||||
height: 40
|
|
||||||
),
|
|
||||||
trailing: Text(part.inStockString),
|
|
||||||
onTap: () {
|
|
||||||
InvenTreePart().get(part.pk).then((var prt) {
|
|
||||||
if (prt is InvenTreePart) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => PartDetailWidget(prt))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildResults(BuildContext context) {
|
|
||||||
|
|
||||||
print("build results");
|
|
||||||
|
|
||||||
if (_searching) {
|
|
||||||
return progressIndicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
search(context);
|
|
||||||
|
|
||||||
if (query.length == 0) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(L10().queryEnter)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.length < 3) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(L10().queryShort),
|
|
||||||
subtitle: Text(L10().queryShortDetail)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (partResults.length == 0) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(L10().noResults),
|
|
||||||
subtitle: Text(L10().queryNoResults + " '${query}'")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: ClampingScrollPhysics(),
|
|
||||||
separatorBuilder: (_, __) => const Divider(height: 3),
|
|
||||||
itemBuilder: _partResult,
|
|
||||||
itemCount: partResults.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildSuggestions(BuildContext context) {
|
|
||||||
// TODO - Implement
|
|
||||||
return Column();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the search theme matches the app theme
|
|
||||||
@override
|
|
||||||
ThemeData appBarTheme(BuildContext context) {
|
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SearchDisplayState extends RefreshableState<SearchWidget> {
|
||||||
class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem?> {
|
|
||||||
|
|
||||||
final stockSearchKey = GlobalKey<ScaffoldState>();
|
|
||||||
|
|
||||||
final BuildContext context;
|
|
||||||
|
|
||||||
String _cachedQuery = "";
|
|
||||||
|
|
||||||
bool _searching = false;
|
|
||||||
|
|
||||||
// Custom filters for the stock item search
|
|
||||||
Map<String, String> _filters = {};
|
|
||||||
|
|
||||||
StockSearchDelegate(this.context, {Map<String, String> filters = const {}}) {
|
|
||||||
|
|
||||||
// Copy filter values
|
|
||||||
for (String key in filters.keys) {
|
|
||||||
|
|
||||||
String? value = filters[key];
|
|
||||||
|
|
||||||
if (value != null) {
|
|
||||||
_filters[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get searchFieldLabel => L10().searchStock;
|
String getAppBarTitle(BuildContext context) => L10().search;
|
||||||
|
|
||||||
// List of StockItem results
|
final TextEditingController searchController = TextEditingController();
|
||||||
List<InvenTreeStockItem> itemResults = [];
|
|
||||||
|
|
||||||
Future<void> search(BuildContext context) async {
|
Timer? debounceTimer;
|
||||||
// Search string too short!
|
|
||||||
if (query.length < 3) {
|
int nPartResults = 0;
|
||||||
itemResults.clear();
|
|
||||||
showResults(context);
|
int nCategoryResults = 0;
|
||||||
return;
|
|
||||||
|
int nStockResults = 0;
|
||||||
|
|
||||||
|
int nLocationResults = 0;
|
||||||
|
|
||||||
|
int nSupplierResults = 0;
|
||||||
|
|
||||||
|
int nPurchaseOrderResults = 0;
|
||||||
|
|
||||||
|
// Callback when the text is being edited
|
||||||
|
// Incorporates a debounce timer to restrict search frequency
|
||||||
|
void onSearchTextChanged(String text, {bool immediate = false}) {
|
||||||
|
|
||||||
|
if (debounceTimer?.isActive ?? false) {
|
||||||
|
debounceTimer!.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query == _cachedQuery) {
|
if (immediate) {
|
||||||
return;
|
search(text);
|
||||||
}
|
} else {
|
||||||
|
debounceTimer = Timer(Duration(milliseconds: 250), () {
|
||||||
_cachedQuery = query;
|
search(text);
|
||||||
|
|
||||||
_searching = true;
|
|
||||||
|
|
||||||
print("Searching...");
|
|
||||||
|
|
||||||
showResults(context);
|
|
||||||
|
|
||||||
// Enable cascading part search by default
|
|
||||||
_filters["cascade"] = "true";
|
|
||||||
|
|
||||||
final results = await InvenTreeStockItem().search(
|
|
||||||
context, query, filters: _filters);
|
|
||||||
|
|
||||||
itemResults.clear();
|
|
||||||
|
|
||||||
for (int idx = 0; idx < results.length; idx++) {
|
|
||||||
if (results[idx] is InvenTreeStockItem) {
|
|
||||||
itemResults.add(results[idx] as InvenTreeStockItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_searching = false;
|
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
"${itemResults.length} ${L10().results}",
|
|
||||||
success: itemResults.length > 0,
|
|
||||||
icon: FontAwesomeIcons.pollH,
|
|
||||||
);
|
|
||||||
|
|
||||||
showSuggestions(context);
|
|
||||||
showResults(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> buildActions(BuildContext context) {
|
|
||||||
return [
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(FontAwesomeIcons.backspace),
|
|
||||||
onPressed: () {
|
|
||||||
query = '';
|
|
||||||
search(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: FaIcon(FontAwesomeIcons.search),
|
|
||||||
onPressed: () {
|
|
||||||
search(context);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildLeading(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
icon: Icon(Icons.arrow_back),
|
|
||||||
onPressed: () {
|
|
||||||
this.close(context, null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _itemResult(BuildContext context, int index) {
|
|
||||||
|
|
||||||
InvenTreeStockItem item = itemResults[index];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
title: Text(item.partName),
|
|
||||||
subtitle: Text(item.locationName),
|
|
||||||
leading: InvenTreeAPI().getImage(
|
|
||||||
item.partThumbnail,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
trailing: Text(item.serialOrQuantityDisplay()),
|
|
||||||
onTap: () {
|
|
||||||
InvenTreeStockItem().get(item.pk).then((var it) {
|
|
||||||
if (it is InvenTreeStockItem) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => StockDetailWidget(it))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> search(String term) async {
|
||||||
|
|
||||||
|
if (term.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
// Do not search on an empty string
|
||||||
|
nPartResults = 0;
|
||||||
|
nCategoryResults = 0;
|
||||||
|
nStockResults = 0;
|
||||||
|
nLocationResults = 0;
|
||||||
|
nSupplierResults = 0;
|
||||||
|
nPurchaseOrderResults = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search parts
|
||||||
|
InvenTreePart().count(
|
||||||
|
searchQuery: term
|
||||||
|
).then((int n) {
|
||||||
|
setState(() {
|
||||||
|
nPartResults = n;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search part categories
|
||||||
|
InvenTreePartCategory().count(
|
||||||
|
searchQuery: term,
|
||||||
|
).then((int n) {
|
||||||
|
setState(() {
|
||||||
|
nCategoryResults = n;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search stock items
|
||||||
|
InvenTreeStockItem().count(
|
||||||
|
searchQuery: term
|
||||||
|
).then((int n) {
|
||||||
|
setState(() {
|
||||||
|
nStockResults = n;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search stock locations
|
||||||
|
InvenTreeStockLocation().count(
|
||||||
|
searchQuery: term
|
||||||
|
).then((int n) {
|
||||||
|
setState(() {
|
||||||
|
nLocationResults = n;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search suppliers
|
||||||
|
InvenTreeCompany().count(
|
||||||
|
searchQuery: term,
|
||||||
|
filters: {
|
||||||
|
"is_supplier": "true",
|
||||||
|
},
|
||||||
|
).then((int n) {
|
||||||
|
setState(() {
|
||||||
|
nSupplierResults = n;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search purchase orders
|
||||||
|
InvenTreePurchaseOrder().count(
|
||||||
|
searchQuery: term,
|
||||||
|
filters: {
|
||||||
|
"outstanding": "true"
|
||||||
|
}
|
||||||
|
).then((int n) {
|
||||||
|
setState(() {
|
||||||
|
nPurchaseOrderResults = n;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _tiles(BuildContext context) {
|
||||||
|
|
||||||
|
List<Widget> tiles = [];
|
||||||
|
|
||||||
|
// Search input
|
||||||
|
tiles.add(
|
||||||
|
InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
title: TextField(
|
||||||
|
readOnly: false,
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: (String text) {
|
||||||
|
onSearchTextChanged(text);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.backspace, color: Colors.red),
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
onSearchTextChanged("", immediate: true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
String query = searchController.text;
|
||||||
|
|
||||||
|
List<Widget> results = [];
|
||||||
|
|
||||||
|
// Part Results
|
||||||
|
if (nPartResults > 0) {
|
||||||
|
results.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().parts),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.shapes),
|
||||||
|
trailing: Text("${nPartResults}"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PartList(
|
||||||
|
{
|
||||||
|
"original_search": query
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part Category Results
|
||||||
|
if (nCategoryResults > 0) {
|
||||||
|
results.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().partCategories),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.sitemap),
|
||||||
|
trailing: Text("${nCategoryResults}"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PartCategoryList(
|
||||||
|
{
|
||||||
|
"original_search": query
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock Item Results
|
||||||
|
if (nStockResults > 0) {
|
||||||
|
results.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().stockItems),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.boxes),
|
||||||
|
trailing: Text("${nStockResults}"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => StockItemList(
|
||||||
|
{
|
||||||
|
"original_search": query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock location results
|
||||||
|
if (nLocationResults > 0) {
|
||||||
|
results.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().stockLocations),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
|
||||||
|
trailing: Text("${nLocationResults}"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => StockLocationList(
|
||||||
|
{
|
||||||
|
"original_search": query
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppliers
|
||||||
|
if (nSupplierResults > 0) {
|
||||||
|
results.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().suppliers),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.building),
|
||||||
|
trailing: Text("${nSupplierResults}"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => CompanyListWidget(
|
||||||
|
L10().suppliers,
|
||||||
|
{
|
||||||
|
"is_supplier": "true",
|
||||||
|
"original_search": query
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purchase orders
|
||||||
|
if (nPurchaseOrderResults > 0) {
|
||||||
|
results.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().purchaseOrders),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.shoppingCart),
|
||||||
|
trailing: Text("${nPurchaseOrderResults}"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PurchaseOrderListWidget(
|
||||||
|
filters: {
|
||||||
|
"original_search": query
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.isEmpty) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().queryNoResults),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.search),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for (Widget result in results) {
|
||||||
|
tiles.add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tiles;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildResults(BuildContext context) {
|
Widget getBody(BuildContext context) {
|
||||||
|
return Center(
|
||||||
search(context);
|
child: ListView(
|
||||||
|
children: ListTile.divideTiles(
|
||||||
if (_searching) {
|
context: context,
|
||||||
return progressIndicator();
|
tiles: _tiles(context),
|
||||||
}
|
).toList()
|
||||||
|
)
|
||||||
search(context);
|
|
||||||
|
|
||||||
if (query.length == 0) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(L10().queryEnter)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.length < 3) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(L10().queryShort),
|
|
||||||
subtitle: Text(L10().queryShortDetail)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemResults.length == 0) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text(L10().noResults),
|
|
||||||
subtitle: Text(L10().queryNoResults + " '${query}'")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: ClampingScrollPhysics(),
|
|
||||||
separatorBuilder: (_, __) => const Divider(height: 3),
|
|
||||||
itemBuilder: _itemResult,
|
|
||||||
itemCount: itemResults.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildSuggestions(BuildContext context) {
|
|
||||||
// TODO - Implement
|
|
||||||
return Column();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the search theme matches the app theme
|
|
||||||
@override
|
|
||||||
ThemeData appBarTheme(BuildContext context) {
|
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -8,16 +8,20 @@
|
|||||||
* | Text <icon> |
|
* | Text <icon> |
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:one_context/one_context.dart';
|
import "package:one_context/one_context.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
|
|
||||||
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
|
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
|
||||||
|
|
||||||
OneContext().hideCurrentSnackBar();
|
BuildContext? context = OneContext().context;
|
||||||
|
|
||||||
|
if (context != null) {
|
||||||
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
|
}
|
||||||
|
|
||||||
Color backgroundColor = Colors.deepOrange;
|
Color backgroundColor = Colors.deepOrange;
|
||||||
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/app_colors.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
|
|
||||||
class Spinner extends StatefulWidget {
|
class Spinner extends StatefulWidget {
|
||||||
final IconData? icon;
|
|
||||||
final Duration duration;
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
const Spinner({
|
const Spinner({
|
||||||
this.color = COLOR_GRAY_LIGHT,
|
this.color = COLOR_GRAY_LIGHT,
|
||||||
@ -16,12 +13,16 @@ class Spinner extends StatefulWidget {
|
|||||||
this.duration = const Duration(milliseconds: 1800),
|
this.duration = const Duration(milliseconds: 1800),
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final IconData? icon;
|
||||||
|
final Duration duration;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_SpinnerState createState() => _SpinnerState();
|
_SpinnerState createState() => _SpinnerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
|
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
|
||||||
AnimationController? _controller;
|
late AnimationController? _controller;
|
||||||
Widget? _child;
|
Widget? _child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:inventree/widget/part_detail.dart";
|
||||||
|
import "package:inventree/widget/progress.dart";
|
||||||
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
import "package:flutter/cupertino.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
import 'package:inventree/inventree/part.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:inventree/widget/part_detail.dart';
|
|
||||||
import 'package:inventree/widget/progress.dart';
|
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
|
||||||
|
|
||||||
import '../api.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class StarredPartWidget extends StatefulWidget {
|
class StarredPartWidget extends StatefulWidget {
|
||||||
|
|
||||||
StarredPartWidget({Key? key}) : super(key: key);
|
const StarredPartWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_StarredPartState createState() => _StarredPartState();
|
_StarredPartState createState() => _StarredPartState();
|
||||||
@ -73,7 +71,7 @@ class _StarredPartState extends RefreshableState<StarredPartWidget> {
|
|||||||
return progressIndicator();
|
return progressIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (starredParts.length == 0) {
|
if (starredParts.isEmpty) {
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
import 'package:inventree/app_colors.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
import 'package:inventree/barcode.dart';
|
import "package:inventree/barcode.dart";
|
||||||
import 'package:inventree/inventree/model.dart';
|
import "package:inventree/inventree/model.dart";
|
||||||
import 'package:inventree/inventree/stock.dart';
|
import "package:inventree/inventree/stock.dart";
|
||||||
import 'package:inventree/inventree/part.dart';
|
import "package:inventree/inventree/part.dart";
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
import "package:inventree/widget/dialogs.dart";
|
||||||
import 'package:inventree/widget/fields.dart';
|
import "package:inventree/widget/fields.dart";
|
||||||
import 'package:inventree/widget/location_display.dart';
|
import "package:inventree/widget/location_display.dart";
|
||||||
import 'package:inventree/widget/part_detail.dart';
|
import "package:inventree/widget/part_detail.dart";
|
||||||
import 'package:inventree/widget/progress.dart';
|
import "package:inventree/widget/progress.dart";
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
import 'package:inventree/widget/snacks.dart';
|
import "package:inventree/widget/snacks.dart";
|
||||||
import 'package:inventree/widget/stock_item_test_results.dart';
|
import "package:inventree/widget/stock_item_test_results.dart";
|
||||||
import 'package:inventree/widget/stock_notes.dart';
|
import "package:inventree/widget/stock_notes.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/helpers.dart";
|
||||||
|
import "package:inventree/api.dart";
|
||||||
|
|
||||||
import 'package:inventree/api.dart';
|
import "package:dropdown_search/dropdown_search.dart";
|
||||||
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:dropdown_search/dropdown_search.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
|
|
||||||
class StockDetailWidget extends StatefulWidget {
|
class StockDetailWidget extends StatefulWidget {
|
||||||
|
|
||||||
StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
const StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
final InvenTreeStockItem item;
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
@ -35,6 +35,8 @@ class StockDetailWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||||
|
|
||||||
|
_StockItemDisplayState(this.item);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().stockItem;
|
String getAppBarTitle(BuildContext context) => L10().stockItem;
|
||||||
|
|
||||||
@ -46,14 +48,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
final _countStockKey = GlobalKey<FormState>();
|
final _countStockKey = GlobalKey<FormState>();
|
||||||
final _moveStockKey = GlobalKey<FormState>();
|
final _moveStockKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
_StockItemDisplayState(this.item);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Widget> getAppBarActions(BuildContext context) {
|
List<Widget> getAppBarActions(BuildContext context) {
|
||||||
|
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('stock', 'view')) {
|
if (InvenTreeAPI().checkPermission("stock", "view")) {
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.globe),
|
icon: FaIcon(FontAwesomeIcons.globe),
|
||||||
@ -62,7 +62,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
@ -99,13 +99,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
await item.reload();
|
await item.reload();
|
||||||
|
|
||||||
// Request part information
|
// Request part information
|
||||||
part = await InvenTreePart().get(item.partId) as InvenTreePart;
|
part = await InvenTreePart().get(item.partId) as InvenTreePart?;
|
||||||
|
|
||||||
// Request test results...
|
// Request test results...
|
||||||
await item.getTestResults();
|
await item.getTestResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editStockItem(BuildContext context) async {
|
Future <void> _editStockItem(BuildContext context) async {
|
||||||
|
|
||||||
var fields = InvenTreeStockItem().formFields();
|
var fields = InvenTreeStockItem().formFields();
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addStock() async {
|
Future <void> _addStock() async {
|
||||||
|
|
||||||
double quantity = double.parse(_quantityController.text);
|
double quantity = double.parse(_quantityController.text);
|
||||||
_quantityController.clear();
|
_quantityController.clear();
|
||||||
@ -138,7 +138,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addStockDialog() async {
|
Future <void> _addStockDialog() async {
|
||||||
|
|
||||||
_quantityController.clear();
|
_quantityController.clear();
|
||||||
_notesController.clear();
|
_notesController.clear();
|
||||||
@ -171,7 +171,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeStock() async {
|
Future <void> _removeStock() async {
|
||||||
|
|
||||||
double quantity = double.parse(_quantityController.text);
|
double quantity = double.parse(_quantityController.text);
|
||||||
_quantityController.clear();
|
_quantityController.clear();
|
||||||
@ -211,7 +211,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _countStock() async {
|
Future <void> _countStock() async {
|
||||||
|
|
||||||
double quantity = double.parse(_quantityController.text);
|
double quantity = double.parse(_quantityController.text);
|
||||||
_quantityController.clear();
|
_quantityController.clear();
|
||||||
@ -223,9 +223,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _countStockDialog() async {
|
Future <void> _countStockDialog() async {
|
||||||
|
|
||||||
_quantityController.text = item.quantityString;
|
_quantityController.text = item.quantity.toString();
|
||||||
_notesController.clear();
|
_notesController.clear();
|
||||||
|
|
||||||
showFormDialog(L10().countStock,
|
showFormDialog(L10().countStock,
|
||||||
@ -251,9 +251,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _unassignBarcode(BuildContext context) async {
|
Future<void> _unassignBarcode(BuildContext context) async {
|
||||||
|
|
||||||
final bool result = await item.update(values: {'uid': ''});
|
final bool result = await item.update(values: {"uid": ""});
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
@ -271,7 +271,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _transferStock(int locationId) async {
|
Future <void> _transferStock(int locationId) async {
|
||||||
|
|
||||||
double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
|
double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
|
||||||
String notes = _notesController.text;
|
String notes = _notesController.text;
|
||||||
@ -288,11 +288,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _transferStockDialog(BuildContext context) async {
|
Future <void> _transferStockDialog(BuildContext context) async {
|
||||||
|
|
||||||
int? location_pk;
|
int? location_pk;
|
||||||
|
|
||||||
_quantityController.text = "${item.quantityString}";
|
_quantityController.text = "${item.quantity}";
|
||||||
|
|
||||||
showFormDialog(L10().transferStock,
|
showFormDialog(L10().transferStock,
|
||||||
key: _moveStockKey,
|
key: _moveStockKey,
|
||||||
@ -327,13 +327,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
},
|
},
|
||||||
onFind: (String filter) async {
|
onFind: (String filter) async {
|
||||||
|
|
||||||
Map<String, String> _filters = {
|
final results = await InvenTreeStockLocation().search(filter);
|
||||||
"search": filter,
|
|
||||||
"offset": "0",
|
|
||||||
"limit": "25"
|
|
||||||
};
|
|
||||||
|
|
||||||
final List<InvenTreeModel> results = await InvenTreeStockLocation().list(filters: _filters);
|
|
||||||
|
|
||||||
List<dynamic> items = [];
|
List<dynamic> items = [];
|
||||||
|
|
||||||
@ -349,13 +343,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
hint: L10().searchLocation,
|
hint: L10().searchLocation,
|
||||||
onChanged: null,
|
onChanged: null,
|
||||||
itemAsString: (dynamic location) {
|
itemAsString: (dynamic location) {
|
||||||
return location['pathstring'];
|
return (location["pathstring"] ?? "") as String;
|
||||||
},
|
},
|
||||||
onSaved: (dynamic location) {
|
onSaved: (dynamic location) {
|
||||||
if (location == null) {
|
if (location == null) {
|
||||||
location_pk = null;
|
location_pk = null;
|
||||||
} else {
|
} else {
|
||||||
location_pk = location['pk'];
|
location_pk = location["pk"] as int;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isFilteredOnline: true,
|
isFilteredOnline: true,
|
||||||
@ -420,7 +414,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().quantity),
|
title: Text(L10().quantity),
|
||||||
leading: FaIcon(FontAwesomeIcons.cubes),
|
leading: FaIcon(FontAwesomeIcons.cubes),
|
||||||
trailing: Text("${item.quantityString}"),
|
trailing: Text("${item.quantityString()}"),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -503,7 +497,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
|
|
||||||
// Supplier part?
|
// Supplier part?
|
||||||
// TODO: Display supplier part info page?
|
// TODO: Display supplier part info page?
|
||||||
if (false && item.supplierPartId > 0) {
|
/*
|
||||||
|
if (item.supplierPartId > 0) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("${item.supplierName}"),
|
title: Text("${item.supplierName}"),
|
||||||
@ -514,6 +509,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if (item.link.isNotEmpty) {
|
if (item.link.isNotEmpty) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
@ -559,7 +555,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
// TODO - Is this stock item linked to a PurchaseOrder?
|
// TODO - Is this stock item linked to a PurchaseOrder?
|
||||||
|
|
||||||
// TODO - Re-enable stock item history display
|
// TODO - Re-enable stock item history display
|
||||||
if (false && item.trackingItemCount > 0) {
|
/*
|
||||||
|
if (item.trackingItemCount > 0) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().history),
|
title: Text(L10().history),
|
||||||
@ -574,6 +571,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Notes field
|
// Notes field
|
||||||
tiles.add(
|
tiles.add(
|
||||||
@ -600,7 +598,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
tiles.add(headerTile());
|
tiles.add(headerTile());
|
||||||
|
|
||||||
// First check that the user has the required permissions to adjust stock
|
// First check that the user has the required permissions to adjust stock
|
||||||
if (!InvenTreeAPI().checkPermission('stock', 'change')) {
|
if (!InvenTreeAPI().checkPermission("stock", "change")) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().permissionRequired),
|
title: Text(L10().permissionRequired),
|
||||||
@ -624,7 +622,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
title: Text(L10().countStock),
|
title: Text(L10().countStock),
|
||||||
leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK),
|
leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK),
|
||||||
onTap: _countStockDialog,
|
onTap: _countStockDialog,
|
||||||
trailing: Text(item.quantityString),
|
trailing: Text(item.quantityString(includeUnits: true)),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -678,12 +676,31 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
|
leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
|
||||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
||||||
|
var handler = UniqueBarcodeHandler((String hash) {
|
||||||
|
item.update(
|
||||||
|
values: {
|
||||||
|
"uid": hash,
|
||||||
|
}
|
||||||
|
).then((result) {
|
||||||
|
if (result) {
|
||||||
|
successTone();
|
||||||
|
|
||||||
|
showSnackIcon(
|
||||||
|
L10().barcodeAssigned,
|
||||||
|
success: true,
|
||||||
|
icon: FontAwesomeIcons.qrcode
|
||||||
|
);
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemBarcodeAssignmentHandler(item)))
|
MaterialPageRoute(builder: (context) => InvenTreeQRView(handler))
|
||||||
).then((context) {
|
);
|
||||||
refresh();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -710,12 +727,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
items: <BottomNavigationBarItem> [
|
items: <BottomNavigationBarItem> [
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.infoCircle),
|
icon: FaIcon(FontAwesomeIcons.infoCircle),
|
||||||
title: Text(L10().details),
|
label: L10().details,
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.wrench),
|
icon: FaIcon(FontAwesomeIcons.wrench),
|
||||||
title: Text(L10().actions),
|
label: L10().actions, ),
|
||||||
),
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,21 @@
|
|||||||
import 'package:inventree/api_form.dart';
|
import "package:inventree/app_colors.dart";
|
||||||
import 'package:inventree/app_colors.dart';
|
import "package:inventree/inventree/part.dart";
|
||||||
import 'package:inventree/inventree/part.dart';
|
import "package:inventree/inventree/stock.dart";
|
||||||
import 'package:inventree/inventree/stock.dart';
|
import "package:inventree/inventree/model.dart";
|
||||||
import 'package:inventree/inventree/model.dart';
|
import "package:inventree/api.dart";
|
||||||
import 'package:inventree/api.dart';
|
import "package:inventree/widget/progress.dart";
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
|
||||||
import 'package:inventree/widget/fields.dart';
|
|
||||||
import 'package:inventree/widget/progress.dart';
|
|
||||||
import 'package:inventree/widget/snacks.dart';
|
|
||||||
|
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
import 'dart:io';
|
import "package:flutter/cupertino.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
import 'package:flutter/material.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class StockItemTestResultsWidget extends StatefulWidget {
|
class StockItemTestResultsWidget extends StatefulWidget {
|
||||||
|
|
||||||
StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
|
const StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
final InvenTreeStockItem item;
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
@ -32,7 +26,7 @@ class StockItemTestResultsWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestResultsWidget> {
|
class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestResultsWidget> {
|
||||||
|
|
||||||
final _addResultKey = GlobalKey<FormState>();
|
_StockItemTestResultDisplayState(this.item);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().testResults;
|
String getAppBarTitle(BuildContext context) => L10().testResults;
|
||||||
@ -57,9 +51,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
|
|
||||||
final InvenTreeStockItem item;
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
_StockItemTestResultDisplayState(this.item);
|
Future <void> addTestResult(BuildContext context, {String name = "", bool nameIsEditable = true, bool result = false, String value = "", bool valueRequired = false, bool attachmentRequired = false}) async {
|
||||||
|
|
||||||
void addTestResult(BuildContext context, {String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
|
|
||||||
|
|
||||||
InvenTreeStockItemTestResult().createForm(
|
InvenTreeStockItemTestResult().createForm(
|
||||||
context,
|
context,
|
||||||
@ -150,7 +142,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
|
|
||||||
var results = getTestResults();
|
var results = getTestResults();
|
||||||
|
|
||||||
if (results.length == 0) {
|
if (results.isEmpty) {
|
||||||
tiles.add(ListTile(
|
tiles.add(ListTile(
|
||||||
title: Text(L10().testResultNone),
|
title: Text(L10().testResultNone),
|
||||||
subtitle: Text(L10().testResultNoneDetail),
|
subtitle: Text(L10().testResultNoneDetail),
|
||||||
@ -165,7 +157,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
String _test = "";
|
String _test = "";
|
||||||
bool _result = false;
|
bool _result = false;
|
||||||
String _value = "";
|
String _value = "";
|
||||||
String _notes = "";
|
|
||||||
|
|
||||||
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
|
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
|
||||||
bool _valueRequired = false;
|
bool _valueRequired = false;
|
||||||
@ -175,8 +166,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
_result = item.passFailStatus();
|
_result = item.passFailStatus();
|
||||||
_test = item.testName;
|
_test = item.testName;
|
||||||
_required = item.required;
|
_required = item.required;
|
||||||
_value = item.latestResult()?.value ?? '';
|
_value = item.latestResult()?.value ?? "";
|
||||||
_notes = item.latestResult()?.notes ?? '';
|
|
||||||
_valueRequired = item.requiresValue;
|
_valueRequired = item.requiresValue;
|
||||||
_attachmentRequired = item.requiresAttachment;
|
_attachmentRequired = item.requiresAttachment;
|
||||||
} else if (item is InvenTreeStockItemTestResult) {
|
} else if (item is InvenTreeStockItemTestResult) {
|
||||||
@ -184,7 +174,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
_test = item.testName;
|
_test = item.testName;
|
||||||
_required = false;
|
_required = false;
|
||||||
_value = item.value;
|
_value = item.value;
|
||||||
_notes = item.notes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_result == true) {
|
if (_result == true) {
|
||||||
|
105
lib/widget/stock_list.dart
Normal file
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:flutter/material.dart";
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import 'package:inventree/inventree/stock.dart';
|
import "package:inventree/inventree/stock.dart";
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
import "package:flutter/cupertino.dart";
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import "package:flutter_markdown/flutter_markdown.dart";
|
||||||
import 'package:inventree/l10.dart';
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
import '../api.dart';
|
import "package:inventree/api.dart";
|
||||||
|
|
||||||
|
|
||||||
class StockNotesWidget extends StatefulWidget {
|
class StockNotesWidget extends StatefulWidget {
|
||||||
|
|
||||||
final InvenTreeStockItem item;
|
const StockNotesWidget(this.item, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
StockNotesWidget(this.item, {Key? key}) : super(key: key);
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_StockNotesState createState() => _StockNotesState(item);
|
_StockNotesState createState() => _StockNotesState(item);
|
||||||
@ -23,10 +23,10 @@ class StockNotesWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _StockNotesState extends RefreshableState<StockNotesWidget> {
|
class _StockNotesState extends RefreshableState<StockNotesWidget> {
|
||||||
|
|
||||||
final InvenTreeStockItem item;
|
|
||||||
|
|
||||||
_StockNotesState(this.item);
|
_StockNotesState(this.item);
|
||||||
|
|
||||||
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().stockItemNotes;
|
String getAppBarTitle(BuildContext context) => L10().stockItemNotes;
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> {
|
|||||||
List<Widget> getAppBarActions(BuildContext context) {
|
List<Widget> getAppBarActions(BuildContext context) {
|
||||||
List<Widget> actions = [];
|
List<Widget> actions = [];
|
||||||
|
|
||||||
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
if (InvenTreeAPI().checkPermission("stock", "change")) {
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
|
import "package:flutter/cupertino.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
|
import "package:inventree/inventree/sentry.dart";
|
||||||
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:inventree/inventree/sentry.dart';
|
|
||||||
import 'package:inventree/widget/snacks.dart';
|
|
||||||
|
|
||||||
import '../l10.dart';
|
|
||||||
|
|
||||||
class SubmitFeedbackWidget extends StatefulWidget {
|
class SubmitFeedbackWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -18,7 +16,7 @@ class SubmitFeedbackWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
|
class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
|
||||||
|
|
||||||
final _formkey = new GlobalKey<FormState>();
|
final _formkey = GlobalKey<FormState>();
|
||||||
|
|
||||||
String message = "";
|
String message = "";
|
||||||
|
|
||||||
@ -61,8 +59,6 @@ class _SubmitFeedbackState extends State<SubmitFeedbackWidget> {
|
|||||||
key: _formkey,
|
key: _formkey,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
30
pubspec.lock
30
pubspec.lock
@ -49,7 +49,21 @@ packages:
|
|||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.1.0"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
camera:
|
camera:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -113,6 +127,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.3"
|
||||||
|
datetime_picker_formfield:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: datetime_picker_formfield
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -315,6 +336,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.3"
|
version: "0.6.3"
|
||||||
|
lint:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: lint
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
45
pubspec.yaml
45
pubspec.yaml
@ -13,40 +13,43 @@ environment:
|
|||||||
sdk: ">=2.12.0 <3.0.0"
|
sdk: ">=2.12.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
||||||
|
audioplayers: ^0.20.1 # Play audio files
|
||||||
|
cached_network_image: ^3.1.0 # Download and cache remote images
|
||||||
|
camera: # Camera
|
||||||
|
cupertino_icons: ^1.0.3
|
||||||
|
datetime_picker_formfield: ^2.0.0 # Date / time picker
|
||||||
|
device_info_plus: ^2.1.0 # Information about the device
|
||||||
|
dropdown_search: 0.6.3 # Dropdown autocomplete form fields
|
||||||
|
file_picker: ^4.0.0 # Select files from the device
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
intl: ^0.17.0
|
|
||||||
|
|
||||||
cupertino_icons: ^1.0.3
|
|
||||||
http: ^0.13.0
|
|
||||||
cached_network_image: ^3.0.0 # Download and cache remote images
|
|
||||||
qr_code_scanner: ^0.5.2 # Barcode scanning
|
|
||||||
package_info_plus: ^1.0.4 # App information introspection
|
|
||||||
device_info_plus: ^2.1.0 # Information about the device
|
|
||||||
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
|
||||||
sentry_flutter: 5.0.0 # Error reporting
|
|
||||||
image_picker: ^0.8.3 # Select or take photos
|
|
||||||
file_picker: ^4.0.0 # Select files from the device
|
|
||||||
url_launcher: 6.0.9 # Open link in system browser
|
|
||||||
open_file: 3.2.1 # Open local files
|
|
||||||
flutter_markdown: ^0.6.2 # Rendering markdown
|
flutter_markdown: ^0.6.2 # Rendering markdown
|
||||||
camera: # Camera
|
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
||||||
path_provider: 2.0.2 # Local file storage
|
http: ^0.13.0
|
||||||
sembast: ^3.1.0+2 # NoSQL data storage
|
image_picker: ^0.8.3 # Select or take photos
|
||||||
one_context: ^1.1.0 # Dialogs without requiring context
|
|
||||||
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
|
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
|
||||||
audioplayers: ^0.20.1 # Play audio files
|
intl: ^0.17.0
|
||||||
dropdown_search: 0.6.3 # Dropdown autocomplete form fields
|
one_context: ^1.1.0 # Dialogs without requiring context
|
||||||
|
open_file: 3.2.1 # Open local files
|
||||||
|
package_info_plus: ^1.0.4 # App information introspection
|
||||||
path:
|
path:
|
||||||
|
path_provider: 2.0.2 # Local file storage
|
||||||
|
qr_code_scanner: ^0.5.2 # Barcode scanning
|
||||||
|
sembast: ^3.1.0+2 # NoSQL data storage
|
||||||
|
sentry_flutter: 5.0.0 # Error reporting
|
||||||
|
url_launcher: 6.0.9 # Open link in system browser
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
flutter_launcher_icons:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_launcher_icons:
|
lint: ^1.0.0
|
||||||
|
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
android: true
|
android: true
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import "package:flutter_test/flutter_test.dart";
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets("Counter increments smoke test", (WidgetTester tester) async {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user