mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 05:26:47 +00:00
Update status codes (#307)
* Extra error info when connecting to server * Adds accessors for various types of status codes * Cleanup / refactor stock status codes - No longer need to duplicate these on the app * improvements to purchase order list - Add option to show closed orders - Render order status * Add purchase order status to order detail widget * Update release notes * Cleanup for imports * linting fixes
This commit is contained in:
parent
efb7ff4170
commit
26b86a2194
@ -7,6 +7,8 @@
|
|||||||
- Fixes keyboard bug in search widget
|
- Fixes keyboard bug in search widget
|
||||||
- Adds ability to create new purchase orders directly from the app
|
- Adds ability to create new purchase orders directly from the app
|
||||||
- Adds support for the "contact" field to purchase orders
|
- Adds support for the "contact" field to purchase orders
|
||||||
|
- Improved rendering of status codes for stock items
|
||||||
|
- Added rendering of status codes for purchase orders
|
||||||
|
|
||||||
### 0.11.0 - April 2023
|
### 0.11.0 - April 2023
|
||||||
---
|
---
|
||||||
|
71
lib/api.dart
71
lib/api.dart
@ -6,6 +6,7 @@ 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:inventree/app_colors.dart";
|
||||||
|
import "package:inventree/inventree/status_codes.dart";
|
||||||
import "package:inventree/preferences.dart";
|
import "package:inventree/preferences.dart";
|
||||||
|
|
||||||
import "package:open_filex/open_filex.dart";
|
import "package:open_filex/open_filex.dart";
|
||||||
@ -148,6 +149,9 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
InvenTreeAPI._internal();
|
InvenTreeAPI._internal();
|
||||||
|
|
||||||
|
// Ensure we only ever create a single instance of the API class
|
||||||
|
static final InvenTreeAPI _api = InvenTreeAPI._internal();
|
||||||
|
|
||||||
// List of callback functions to trigger when the connection status changes
|
// List of callback functions to trigger when the connection status changes
|
||||||
List<Function()> _statusCallbacks = [];
|
List<Function()> _statusCallbacks = [];
|
||||||
|
|
||||||
@ -346,9 +350,6 @@ class InvenTreeAPI {
|
|||||||
return !isConnected() && _connecting;
|
return !isConnected() && _connecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we only ever create a single instance of the API class
|
|
||||||
static final InvenTreeAPI _api = InvenTreeAPI._internal();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Connect to the remote InvenTree server:
|
* Connect to the remote InvenTree server:
|
||||||
*
|
*
|
||||||
@ -490,11 +491,33 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
debug("Received token from server");
|
debug("Received token from server");
|
||||||
|
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
// Request user role information (async)
|
// Request user role information (async)
|
||||||
getUserRoles();
|
result = await getUserRoles();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
showServerError(
|
||||||
|
apiUrl,
|
||||||
|
L10().serverError,
|
||||||
|
L10().errorUserRoles,
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Request plugin information (async)
|
// Request plugin information (async)
|
||||||
getPluginInformation();
|
result = await getPluginInformation();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
showServerError(
|
||||||
|
apiUrl,
|
||||||
|
L10().serverError,
|
||||||
|
L10().errorPluginInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Ok, probably pretty good...
|
// Ok, probably pretty good...
|
||||||
return true;
|
return true;
|
||||||
@ -516,9 +539,7 @@ class InvenTreeAPI {
|
|||||||
_connectionStatusChanged();
|
_connectionStatusChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// Public facing connection function
|
||||||
* Public facing connection function
|
|
||||||
*/
|
|
||||||
Future<bool> connectToServer() async {
|
Future<bool> connectToServer() async {
|
||||||
|
|
||||||
// Ensure server is first disconnected
|
// Ensure server is first disconnected
|
||||||
@ -590,12 +611,12 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Request plugin information from the server
|
// Request plugin information from the server
|
||||||
Future<void> getPluginInformation() async {
|
Future<bool> getPluginInformation() async {
|
||||||
|
|
||||||
// The server does not support plugins, or they are not enabled
|
// The server does not support plugins, or they are not enabled
|
||||||
if (!pluginsEnabled()) {
|
if (!pluginsEnabled()) {
|
||||||
_plugins.clear();
|
_plugins.clear();
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("API: getPluginInformation()");
|
debug("API: getPluginInformation()");
|
||||||
@ -611,15 +632,15 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check if the user has the given role.permission assigned
|
||||||
|
* e.g. "part", "change"
|
||||||
|
*/
|
||||||
bool checkPermission(String role, String permission) {
|
bool checkPermission(String role, String permission) {
|
||||||
/*
|
|
||||||
* Check if the user has the given role.permission assigned
|
|
||||||
*e
|
|
||||||
* 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
|
||||||
if (roles.isEmpty) {
|
if (roles.isEmpty) {
|
||||||
return true;
|
return true;
|
||||||
@ -1422,4 +1443,22 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep an internal map of status codes
|
||||||
|
Map<String, InvenTreeStatusCode> _status_codes = {};
|
||||||
|
|
||||||
|
// Return a status class based on provided URL
|
||||||
|
InvenTreeStatusCode _get_status_class(String url) {
|
||||||
|
if (!_status_codes.containsKey(url)) {
|
||||||
|
_status_codes[url] = InvenTreeStatusCode(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _status_codes[url]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessors methods for various status code classes
|
||||||
|
InvenTreeStatusCode get StockHistoryStatus => _get_status_class("stock/track/status/");
|
||||||
|
InvenTreeStatusCode get StockStatus => _get_status_class("stock/status/");
|
||||||
|
InvenTreeStatusCode get PurchaseOrderStatus => _get_status_class("order/po/status/");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
112
lib/inventree/status_codes.dart
Normal file
112
lib/inventree/status_codes.dart
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Code for querying the server for various status code data,
|
||||||
|
* so that we do not have to duplicate those codes in the app.
|
||||||
|
*
|
||||||
|
* Ref: https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/status_codes.py
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
import "package:inventree/api.dart";
|
||||||
|
import "package:inventree/helpers.dart";
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Base class definition for a "status code" definition.
|
||||||
|
*/
|
||||||
|
class InvenTreeStatusCode {
|
||||||
|
|
||||||
|
InvenTreeStatusCode(this.URL);
|
||||||
|
|
||||||
|
final String URL;
|
||||||
|
|
||||||
|
// Internal status code data loaded from server
|
||||||
|
Map<String, dynamic> data = {};
|
||||||
|
|
||||||
|
// Load status code information from the server
|
||||||
|
Future<void> load({bool forceReload = false}) async {
|
||||||
|
|
||||||
|
// Return internally cached data
|
||||||
|
if (data.isNotEmpty && !forceReload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server must support this feature!
|
||||||
|
if (!InvenTreeAPI().supportsStatusLabelEndpoints) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("Loading status codes from ${URL}");
|
||||||
|
|
||||||
|
APIResponse response = await InvenTreeAPI().get(URL);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
Map<String, dynamic> results = response.data as Map<String, dynamic>;
|
||||||
|
|
||||||
|
if (results.containsKey("values")) {
|
||||||
|
data = results["values"] as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the entry associated with the provided integer status
|
||||||
|
Map<String, dynamic> entry(int status) {
|
||||||
|
for (String key in data.keys) {
|
||||||
|
dynamic _entry = data[key];
|
||||||
|
|
||||||
|
if (_entry is Map<String, dynamic>) {
|
||||||
|
dynamic _status = _entry["key"];
|
||||||
|
|
||||||
|
if (_status is int) {
|
||||||
|
if (status == _status) {
|
||||||
|
return _entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match - return an empty map
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the 'label' associated with a given status code
|
||||||
|
String label(int status) {
|
||||||
|
Map<String, dynamic> _entry = entry(status);
|
||||||
|
|
||||||
|
String _label = (_entry["label"] ?? "") as String;
|
||||||
|
|
||||||
|
if (_label.isEmpty) {
|
||||||
|
// If no match found, return the status code
|
||||||
|
debug("No match for status code ${status} at '${URL}'");
|
||||||
|
return status.toString();
|
||||||
|
} else {
|
||||||
|
return _label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the 'color' associated with a given status code
|
||||||
|
Color color(int status) {
|
||||||
|
Map<String, dynamic> _entry = entry(status);
|
||||||
|
|
||||||
|
String color_name = (_entry["color"] ?? "") as String;
|
||||||
|
|
||||||
|
switch (color_name.toLowerCase()) {
|
||||||
|
case "success":
|
||||||
|
return Colors.green;
|
||||||
|
case "primary":
|
||||||
|
return Colors.blue;
|
||||||
|
case "secondary":
|
||||||
|
return Colors.grey;
|
||||||
|
case "dark":
|
||||||
|
return Colors.black;
|
||||||
|
case "danger":
|
||||||
|
return Colors.red;
|
||||||
|
case "warning":
|
||||||
|
return Colors.orange;
|
||||||
|
case "info":
|
||||||
|
return Colors.lightBlue;
|
||||||
|
default:
|
||||||
|
return Colors.black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import "dart:async";
|
import "dart:async";
|
||||||
|
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
import "package:inventree/inventree/part.dart";
|
import "package:inventree/inventree/part.dart";
|
||||||
@ -122,66 +121,10 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||||
|
|
||||||
// Stock status codes
|
|
||||||
static const int OK = 10;
|
|
||||||
static const int ATTENTION = 50;
|
|
||||||
static const int DAMAGED = 55;
|
|
||||||
static const int DESTROYED = 60;
|
|
||||||
static const int REJECTED = 65;
|
|
||||||
static const int LOST = 70;
|
|
||||||
static const int QUARANTINED = 75;
|
|
||||||
static const int RETURNED = 85;
|
|
||||||
|
|
||||||
String statusLabel() {
|
|
||||||
|
|
||||||
// TODO: Delete me - The translated status values should be provided by the API!
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case OK:
|
|
||||||
return L10().ok;
|
|
||||||
case ATTENTION:
|
|
||||||
return L10().attention;
|
|
||||||
case DAMAGED:
|
|
||||||
return L10().damaged;
|
|
||||||
case DESTROYED:
|
|
||||||
return L10().destroyed;
|
|
||||||
case REJECTED:
|
|
||||||
return L10().rejected;
|
|
||||||
case LOST:
|
|
||||||
return L10().lost;
|
|
||||||
case QUARANTINED:
|
|
||||||
return L10().quarantined;
|
|
||||||
case RETURNED:
|
|
||||||
return L10().returned;
|
|
||||||
default:
|
|
||||||
return status.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return color associated with stock status
|
|
||||||
Color get statusColor {
|
|
||||||
switch (status) {
|
|
||||||
case OK:
|
|
||||||
return Colors.black;
|
|
||||||
case ATTENTION:
|
|
||||||
return Color(0xFFfdc82a);
|
|
||||||
case DAMAGED:
|
|
||||||
case DESTROYED:
|
|
||||||
case REJECTED:
|
|
||||||
return Color(0xFFe35a57);
|
|
||||||
case QUARANTINED:
|
|
||||||
return Color(0xFF0DCAF0);
|
|
||||||
case LOST:
|
|
||||||
default:
|
|
||||||
return Color(0xFFAAAAAA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get URL => "stock/";
|
String get URL => "stock/";
|
||||||
|
|
||||||
// URLs for performing stock actions
|
// URLs for performing stock actions
|
||||||
|
|
||||||
static String transferStockUrl() => "stock/transfer/";
|
static String transferStockUrl() => "stock/transfer/";
|
||||||
|
|
||||||
static String countStockUrl() => "stock/count/";
|
static String countStockUrl() => "stock/count/";
|
||||||
|
@ -288,6 +288,12 @@
|
|||||||
"errorFetch": "Error fetching data from server",
|
"errorFetch": "Error fetching data from server",
|
||||||
"@errorFetch": {},
|
"@errorFetch": {},
|
||||||
|
|
||||||
|
"errorUserRoles": "Error requesting user roles from server",
|
||||||
|
"@errorUserRoles": {},
|
||||||
|
|
||||||
|
"errorPluginInfo": "Error requesting plugin data from server",
|
||||||
|
"@errorPluginInfo": {},
|
||||||
|
|
||||||
"errorReporting": "Error Reporting",
|
"errorReporting": "Error Reporting",
|
||||||
"@errorReporting": {},
|
"@errorReporting": {},
|
||||||
|
|
||||||
@ -579,6 +585,12 @@
|
|||||||
"onOrderDetails": "Items currently on order",
|
"onOrderDetails": "Items currently on order",
|
||||||
"@onOrderDetails": {},
|
"@onOrderDetails": {},
|
||||||
|
|
||||||
|
"outstanding": "Outstanding",
|
||||||
|
"@outstanding": {},
|
||||||
|
|
||||||
|
"outstandingOrderDetail": "Show outstanding orders",
|
||||||
|
"@outstandingOrderDetail": {},
|
||||||
|
|
||||||
"packaging": "Packaging",
|
"packaging": "Packaging",
|
||||||
"@packaging": {},
|
"@packaging": {},
|
||||||
|
|
||||||
|
@ -66,6 +66,8 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
|
|||||||
Future<void> request(BuildContext context) async {
|
Future<void> request(BuildContext context) async {
|
||||||
await order.reload();
|
await order.reload();
|
||||||
|
|
||||||
|
await api.PurchaseOrderStatus.load();
|
||||||
|
|
||||||
lines = await order.getLineItems();
|
lines = await order.getLineItems();
|
||||||
|
|
||||||
completedLines = 0;
|
completedLines = 0;
|
||||||
@ -112,6 +114,12 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
|
|||||||
title: Text(order.reference),
|
title: Text(order.reference),
|
||||||
subtitle: Text(order.description),
|
subtitle: Text(order.description),
|
||||||
leading: supplier == null ? null : InvenTreeAPI().getImage(supplier.thumbnail, width: 40, height: 40),
|
leading: supplier == null ? null : InvenTreeAPI().getImage(supplier.thumbnail, width: 40, height: 40),
|
||||||
|
trailing: Text(
|
||||||
|
api.PurchaseOrderStatus.label(order.status),
|
||||||
|
style: TextStyle(
|
||||||
|
color: api.PurchaseOrderStatus.color(order.status)
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -125,11 +125,19 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur
|
|||||||
"target_date": L10().targetDate,
|
"target_date": L10().targetDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Map<String, dynamic>> get filterOptions => {
|
||||||
|
"outstanding": {
|
||||||
|
"label": L10().outstanding,
|
||||||
|
"help_text": L10().outstandingOrderDetail,
|
||||||
|
"tristate": true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
||||||
|
|
||||||
params["outstanding"] = "true";
|
await InvenTreeAPI().PurchaseOrderStatus.load();
|
||||||
|
|
||||||
final page = await InvenTreePurchaseOrder().listPaginated(limit, offset, filters: params);
|
final page = await InvenTreePurchaseOrder().listPaginated(limit, offset, filters: params);
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
@ -151,7 +159,12 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
),
|
),
|
||||||
trailing: Text("${order.lineItemCount}"),
|
trailing: Text(
|
||||||
|
InvenTreeAPI().PurchaseOrderStatus.label(order.status),
|
||||||
|
style: TextStyle(
|
||||||
|
color: InvenTreeAPI().PurchaseOrderStatus.color(order.status),
|
||||||
|
),
|
||||||
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
@ -211,6 +211,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
@override
|
@override
|
||||||
Future<void> request(BuildContext context) async {
|
Future<void> request(BuildContext context) async {
|
||||||
|
|
||||||
|
await api.StockStatus.load();
|
||||||
|
|
||||||
stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool;
|
stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool;
|
||||||
|
|
||||||
final bool result = widget.item.pk > 0 && await widget.item.reload();
|
final bool result = widget.item.pk > 0 && await widget.item.reload();
|
||||||
@ -565,9 +567,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
subtitle: Text("${widget.item.partDescription}"),
|
subtitle: Text("${widget.item.partDescription}"),
|
||||||
leading: InvenTreeAPI().getImage(widget.item.partImage),
|
leading: InvenTreeAPI().getImage(widget.item.partImage),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
widget.item.statusLabel(),
|
api.StockStatus.label(widget.item.status),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: widget.item.statusColor
|
color: api.StockStatus.color(widget.item.status),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
@ -33,7 +33,7 @@ class _StockItemHistoryDisplayState extends RefreshableState<StockItemHistoryWid
|
|||||||
|
|
||||||
history.clear();
|
history.clear();
|
||||||
|
|
||||||
InvenTreeStockItemHistory().list(filters: {"item": "${item.pk}"}).then((List<InvenTreeModel> results) {
|
await InvenTreeStockItemHistory().list(filters: {"item": "${item.pk}"}).then((List<InvenTreeModel> results) {
|
||||||
for (var result in results) {
|
for (var result in results) {
|
||||||
if (result is InvenTreeStockItemHistory) {
|
if (result is InvenTreeStockItemHistory) {
|
||||||
history.add(result);
|
history.add(result);
|
||||||
|
@ -95,6 +95,9 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
|||||||
@override
|
@override
|
||||||
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
||||||
|
|
||||||
|
// Ensure StockStatus codes are loaded
|
||||||
|
await InvenTreeAPI().StockStatus.load();
|
||||||
|
|
||||||
final page = await InvenTreeStockItem().listPaginated(
|
final page = await InvenTreeStockItem().listPaginated(
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
@ -120,7 +123,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
|||||||
trailing: Text("${item.displayQuantity}",
|
trailing: Text("${item.displayQuantity}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: item.statusColor,
|
color: InvenTreeAPI().StockStatus.color(item.status),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user