2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 13:36:50 +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:
Oliver 2023-04-10 23:12:30 +10:00 committed by GitHub
parent efb7ff4170
commit 26b86a2194
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 80 deletions

View File

@ -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
--- ---

View File

@ -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/");
} }

View 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;
}
}
}

View File

@ -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/";

View File

@ -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": {},

View File

@ -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)
),
)
) )
); );

View File

@ -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,

View File

@ -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 {

View File

@ -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);

View File

@ -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: () {