2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-27 21:16:48 +00:00

Stock display (#379)

* Display stock quantity more prominently

* Cleanup search widget

* Update for stock_detail widget

* More tweaks

* Change bottom bar icon

* Display boolean parameters appropriately

* Adds ability to edit part parameters

* Bump icon size a bit

* Improvements to filter options

- Allow filtering by "option" type
- To start with, filter stock by status code

* Remove debug message

* Remove getTriState method

- No longer needed
- Remove associated unit tests

* Adjust filters based on server API version

* Muted colors
This commit is contained in:
Oliver 2023-06-24 11:34:42 +10:00 committed by GitHub
parent 8076887e39
commit e9eb84eace
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 278 additions and 167 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ coverage/*
# This file is auto-generated as part of the CI process # This file is auto-generated as part of the CI process
test/coverage_helper_test.dart test/coverage_helper_test.dart
InvenTreeSettings.db
# Sentry API key # Sentry API key
lib/dsn.dart lib/dsn.dart

View File

@ -1,6 +1,8 @@
### - ### -
--- ---
- Edit part parameters from within the app
- Increase visibility of stock quantity in widgets
- Improved filters for stock list - Improved filters for stock list
### 0.12.2 - June 2023 ### 0.12.2 - June 2023

View File

@ -241,6 +241,7 @@
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Thin Binary"; name = "Thin Binary";
outputPaths = ( outputPaths = (

View File

@ -1332,7 +1332,20 @@ class InvenTreeAPI {
static String get staticThumb => "/static/img/blank_image.thumbnail.png"; static String get staticThumb => "/static/img/blank_image.thumbnail.png";
CachedNetworkImage getThumbnail(String imageUrl, {double size = 40}) => getImage(imageUrl, width: size, height: size); CachedNetworkImage? getThumbnail(String imageUrl, {double size = 40, bool hideIfNull = false}) {
if (hideIfNull) {
if (imageUrl.isEmpty) {
return null;
}
}
return getImage(
imageUrl,
width: size,
height: size
);
}
/* /*
* Load image from the InvenTree server, * Load image from the InvenTree server,

View File

@ -275,6 +275,7 @@ class APIFormField {
// Construct a widget for this input // Construct a widget for this input
Widget constructField(BuildContext context) { Widget constructField(BuildContext context) {
switch (type) { switch (type) {
case "string": case "string":
case "url": case "url":
@ -696,6 +697,14 @@ class APIFormField {
// Construct a string input element // Construct a string input element
Widget _constructString() { Widget _constructString() {
if (readOnly) {
return ListTile(
title: Text(label),
subtitle: Text(helpText),
trailing: Text(value.toString()),
);
}
return TextFormField( return TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: required ? label + "*" : label, labelText: required ? label + "*" : label,
@ -724,12 +733,21 @@ class APIFormField {
// Construct a boolean input element // Construct a boolean input element
Widget _constructBoolean() { Widget _constructBoolean() {
bool? initial_value;
if (value is bool || value == null) {
initial_value = value as bool?;
} else {
String vs = value.toString().toLowerCase();
initial_value = ["1", "true", "yes"].contains(vs);
}
return CheckBoxField( return CheckBoxField(
label: label, label: label,
labelStyle: _labelStyle(), labelStyle: _labelStyle(),
helperText: helpText, helperText: helpText,
helperStyle: _helperStyle(), helperStyle: _helperStyle(),
initial: value as bool?, initial: initial_value,
tristate: (getParameter("tristate") ?? false) as bool, tristate: (getParameter("tristate") ?? false) as bool,
onSaved: (val) { onSaved: (val) {
data["value"] = val; data["value"] = val;
@ -1262,6 +1280,10 @@ class _APIFormWidgetState extends State<APIFormWidget> {
for (var field in widget.fields) { for (var field in widget.fields) {
if (field.readOnly) {
continue;
}
if (field.isSimple) { if (field.isSimple) {
// Simple top-level field data // Simple top-level field data
data[field.name] = field.data["value"]; data[field.name] = field.data["value"];

View File

@ -16,6 +16,6 @@ Color get COLOR_ACTION {
} }
const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1); const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1); const Color COLOR_DANGER = Color.fromRGBO(200, 50, 75, 1);
const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1); const Color COLOR_SUCCESS = Color.fromRGBO(100, 200, 75, 1);
const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1); const Color COLOR_PROGRESS = Color.fromRGBO(50, 100, 200, 1);

View File

@ -133,12 +133,29 @@ class InvenTreePartParameter extends InvenTreeModel {
@override @override
String get URL => "part/parameter/"; String get URL => "part/parameter/";
@override
List<String> get rolesRequired => ["part"];
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartParameter.fromJson(json); InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartParameter.fromJson(json);
@override @override
Map<String, dynamic> formFields() { Map<String, dynamic> formFields() {
return {};
Map<String, dynamic> fields = {
"header": {
"type": "string",
"read_only": true,
"label": name,
"help_text": description,
"value": "",
},
"data": {
"type": "string",
}
};
return fields;
} }
@override @override
@ -160,7 +177,11 @@ class InvenTreePartParameter extends InvenTreeModel {
return v; return v;
} }
bool get as_bool => value.toLowerCase() == "true";
String get units => getString("units", subKey: "template_detail"); String get units => getString("units", subKey: "template_detail");
bool get is_checkbox => getBool("checkbox", subKey: "template_detail", backup: false);
} }
/* /*

View File

@ -23,6 +23,26 @@ class InvenTreeStatusCode {
// Internal status code data loaded from server // Internal status code data loaded from server
Map<String, dynamic> data = {}; Map<String, dynamic> data = {};
/*
* Construct a list of "choices" suitable for a form
*/
List<dynamic> get choices {
List<dynamic> _choices = [];
for (String key in data.keys) {
dynamic _entry = data[key];
if (_entry is Map<String, dynamic>) {
_choices.add({
"value": _entry["key"],
"display_name": _entry["label"]
});
}
}
return _choices;
}
// Load status code information from the server // Load status code information from the server
Future<void> load({bool forceReload = false}) async { Future<void> load({bool forceReload = false}) async {

View File

@ -399,7 +399,7 @@ class InvenTreeStockItem extends InvenTreeModel {
double get quantity => getDouble("quantity"); double get quantity => getDouble("quantity");
String quantityString({bool includeUnits = false}){ String quantityString({bool includeUnits = true}){
String q = ""; String q = "";
@ -467,7 +467,13 @@ class InvenTreeStockItem extends InvenTreeModel {
if (serialNumber.isNotEmpty) { if (serialNumber.isNotEmpty) {
return "SN: $serialNumber"; return "SN: $serialNumber";
} else { } else {
return simpleNumberString(quantity); String q = simpleNumberString(quantity);
if (units.isNotEmpty) {
q += " ${units}";
}
return q;
} }
} }

View File

@ -278,6 +278,9 @@
"editNotes": "Edit Notes", "editNotes": "Edit Notes",
"@editNotes": {}, "@editNotes": {},
"editParameter": "Edit Parameter",
"@editParameter": {},
"editPart": "Edit Part", "editPart": "Edit Part",
"@editPart": { "@editPart": {
"description": "edit part" "description": "edit part"

View File

@ -126,7 +126,12 @@ class InvenTreeSettingsManager {
Future<dynamic> getValue(String key, dynamic backup) async { Future<dynamic> getValue(String key, dynamic backup) async {
final value = await store.record(key).get(await _db); dynamic value = await store.record(key).get(await _db);
// Retrieve value
if (value == "__null__") {
value = null;
}
if (value == null) { if (value == null) {
return backup; return backup;
@ -148,32 +153,11 @@ class InvenTreeSettingsManager {
} }
} }
// Load a tristate (true / false / null) setting
Future<bool?> getTriState(String key, dynamic backup) async {
final dynamic value = await getValue(key, backup);
if (value == null) {
return null;
} else if (value is bool) {
return value;
} else {
String s = value.toString().toLowerCase();
if (s.contains("t")) {
return true;
} else if (s.contains("f")) {
return false;
} else {
return null;
}
}
}
// Store a key:value pair in the database // Store a key:value pair in the database
Future<void> setValue(String key, dynamic value) async { Future<void> setValue(String key, dynamic value) async {
// Encode null values as strings // Encode null values as strings
value ??= "null"; value ??= "__null__";
await store.record(key).put(await _db, value); await store.record(key).put(await _db, value);
} }

View File

@ -154,8 +154,8 @@ class CheckBoxField extends FormField<bool> {
onSaved: onSaved, onSaved: onSaved,
initialValue: initial, initialValue: initial,
builder: (FormFieldState<bool> state) { builder: (FormFieldState<bool> state) {
return CheckboxListTile( return CheckboxListTile(
//dense: state.hasError,
title: label != null ? Text(label, style: labelStyle) : null, title: label != null ? Text(label, style: labelStyle) : null,
value: state.value, value: state.value,
tristate: tristate, tristate: tristate,

View File

@ -38,44 +38,39 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Override in implementing class // Override in implementing class
String get prefix => "prefix_"; String get prefix => "prefix_";
// Return a map of boolean filtering options available for this list
// Should be overridden by an implementing subclass // Should be overridden by an implementing subclass
Map<String, Map<String, dynamic>> get filterOptions => {}; Map<String, Map<String, dynamic>> get filterOptions => {};
// Return the boolean value of a particular boolean filter // Return the boolean value of a particular boolean filter
Future<bool?> getBooleanFilterValue(String key) async { Future<dynamic> getFilterValue(String key) async {
key = "${prefix}bool_${key}"; key = "${prefix}filter_${key}";
Map<String, dynamic> opts = filterOptions[key] ?? {}; Map<String, dynamic> opts = filterOptions[key] ?? {};
dynamic backup = opts["default"];
final result = await InvenTreeSettingsManager().getValue(key, backup);
bool? backup;
dynamic v = opts["default"];
if (v is bool) {
backup = v;
}
final result = await InvenTreeSettingsManager().getTriState(key, backup);
return result; return result;
} }
// Set the boolean value of a particular boolean filter // Set the boolean value of a particular boolean filter
Future<void> setBooleanFilterValue(String key, bool? value) async { Future<void> setFilterValue(String key, dynamic value) async {
key = "${prefix}bool_${key}"; key = "${prefix}filter_${key}";
await InvenTreeSettingsManager().setValue(key, value); await InvenTreeSettingsManager().setValue(key, value);
} }
// Construct the boolean filter options for this list // Construct the boolean filter options for this list
Future<Map<String, String>> constructBooleanFilters() async { Future<Map<String, String>> constructFilters() async {
Map<String, String> f = {}; Map<String, String> f = {};
for (String k in filterOptions.keys) { for (String k in filterOptions.keys) {
bool? value = await getBooleanFilterValue(k); dynamic value = await getFilterValue(k);
if (value is bool) { // Skip null values
f[k] = value ? "true" : "false"; if (value == null) {
continue;
} }
f[k] = value.toString();
} }
return f; return f;
@ -164,7 +159,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
} }
}; };
// Add in boolean filter options // Add in selected filter options
for (String key in filterOptions.keys) { for (String key in filterOptions.keys) {
Map<String, dynamic> opts = filterOptions[key] ?? {}; Map<String, dynamic> opts = filterOptions[key] ?? {};
@ -172,17 +167,18 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
String label = (opts["label"] ?? key) as String; String label = (opts["label"] ?? key) as String;
String? help_text = opts["help_text"] as String?; String? help_text = opts["help_text"] as String?;
List<dynamic> choices = (opts["choices"] ?? []) as List<dynamic>;
bool tristate = (opts["tristate"] ?? true) as bool; bool tristate = (opts["tristate"] ?? true) as bool;
bool? v = await getBooleanFilterValue(key); dynamic v = await getFilterValue(key);
// Prevent null value if not tristate // Prevent null value if not tristate
if (!tristate && v == null) { if (!tristate && v == null) {
v = false; v = false;
} }
// Add in the particular field Map<String, dynamic> filter = {
fields[key] = {
"type": "boolean", "type": "boolean",
"display_name": label, "display_name": label,
"label": label, "label": label,
@ -190,6 +186,16 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
"value": v, "value": v,
"tristate": (opts["tristate"] ?? true) as bool, "tristate": (opts["tristate"] ?? true) as bool,
}; };
if (choices.isNotEmpty) {
// Configure as a choice input
filter["type"] = "choice";
filter["choices"] = choices;
filter.remove("tristate");
}
fields[key] = filter;
} }
// Launch an interactive form for the user to select options // Launch an interactive form for the user to select options
@ -211,16 +217,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Save boolean fields // Save boolean fields
for (String key in filterOptions.keys) { for (String key in filterOptions.keys) {
await setFilterValue(key, data[key]);
bool? v;
dynamic value = data[key];
if (value is bool) {
v = value;
}
await setBooleanFilterValue(key, v);
} }
// Refresh data from the server // Refresh data from the server
@ -293,7 +290,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
params["ordering"] = o; params["ordering"] = o;
} }
Map<String, String> f = await constructBooleanFilters(); Map<String, String> f = await constructFilters();
if (f.isNotEmpty) { if (f.isNotEmpty) {
params.addAll(f); params.addAll(f);
@ -348,6 +345,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
void updateSearchTerm() { void updateSearchTerm() {
searchTerm = searchController.text; searchTerm = searchController.text;
_pagingController.refresh(); _pagingController.refresh();
if (mounted) {
setState(() {});
}
} }
// Function to construct a single paginated item // Function to construct a single paginated item
@ -409,19 +410,19 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
*/ */
Widget buildSearchInput(BuildContext context) { Widget buildSearchInput(BuildContext context) {
return ListTile( return ListTile(
trailing: orderingOptions.isEmpty ? null : GestureDetector( leading: orderingOptions.isEmpty ? null : GestureDetector(
child: FaIcon(FontAwesomeIcons.sort, color: COLOR_ACTION), child: Icon(Icons.filter_list, color: COLOR_ACTION, size: 32),
onTap: () async { onTap: () async {
_saveOrderingOptions(context); _saveOrderingOptions(context);
}, },
), ),
leading: GestureDetector( trailing: GestureDetector(
child: FaIcon( child: FaIcon(
searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft, searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft,
color: searchController.text.isNotEmpty ? COLOR_DANGER : null, color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_ACTION,
), ),
onTap: () { onTap: () {
if (searchController.text.isEmpty) { if (searchController.text.isNotEmpty) {
searchController.clear(); searchController.clear();
} }
updateSearchTerm(); updateSearchTerm();

View File

@ -228,20 +228,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
}); });
} }
/*
* Toggle the "star" status of this paricular part
*/
Future <void> _toggleStar(BuildContext context) async {
if (InvenTreePart().canView) {
showLoadingOverlay(context);
await part.update(values: {"starred": "${!part.starred}"});
hideLoadingOverlay();
refresh(context);
}
}
void _editPartDialog(BuildContext context) { void _editPartDialog(BuildContext context) {
part.editForm( part.editForm(
@ -259,13 +245,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
child: ListTile( child: ListTile(
title: Text("${part.fullname}"), title: Text("${part.fullname}"),
subtitle: Text("${part.description}"), subtitle: Text("${part.description}"),
trailing: IconButton( trailing: Text(
icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star, part.stockString(),
color: part.starred ? Colors.yellowAccent : null, style: TextStyle(
), fontSize: 20,
onPressed: () { )
_toggleStar(context);
},
), ),
leading: GestureDetector( leading: GestureDetector(
child: api.getImage(part.thumbnail), child: api.getImage(part.thumbnail),

View File

@ -133,7 +133,13 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
return ListTile( return ListTile(
title: Text(part.fullname), title: Text(part.fullname),
subtitle: Text(part.description), subtitle: Text(part.description),
trailing: Text(part.stockString()), trailing: Text(
part.stockString(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold
)
),
leading: InvenTreeAPI().getThumbnail(part.thumbnail), leading: InvenTreeAPI().getThumbnail(part.thumbnail),
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));

View File

@ -4,6 +4,7 @@ import "package:inventree/inventree/model.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/part.dart";
import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
/* /*
@ -75,7 +76,7 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
@override @override
Map<String, String> get orderingOptions => { Map<String, String> get orderingOptions => {
// TODO
}; };
@override @override
@ -91,6 +92,22 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
return page; return page;
} }
Future<void> editParameter(InvenTreePartParameter parameter) async {
// Checkbox values are handled separately
if (parameter.is_checkbox) {
return;
} else {
parameter.editForm(
context,
L10().editParameter,
onSuccess: (data) async {
updateSearchTerm();
}
);
}
}
@override @override
Widget buildItem(BuildContext context, InvenTreeModel model) { Widget buildItem(BuildContext context, InvenTreeModel model) {
@ -99,7 +116,28 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
return ListTile( return ListTile(
title: Text(parameter.name), title: Text(parameter.name),
subtitle: Text(parameter.description), subtitle: Text(parameter.description),
trailing: Text(parameter.valueString), trailing: parameter.is_checkbox
? Switch(
value: parameter.as_bool,
onChanged: (bool value) {
if (parameter.canEdit) {
showLoadingOverlay(context);
parameter.update(
values: {
"data": value.toString()
}
).then((value) async{
hideLoadingOverlay();
updateSearchTerm();
});
}
},
) : Text(parameter.valueString),
onTap: parameter.is_checkbox ? null : () async {
if (parameter.canEdit) {
editParameter(parameter);
}
},
); );
} }
} }

View File

@ -71,7 +71,7 @@ mixin BaseWidgetProperties {
*/ */
BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
const double iconSize = 32; const double iconSize = 40;
List<Widget> icons = [ List<Widget> icons = [
IconButton( IconButton(
@ -98,7 +98,7 @@ mixin BaseWidgetProperties {
}, },
), ),
IconButton( IconButton(
icon: Icon(Icons.qr_code_scanner, color: COLOR_ACTION), icon: Icon(Icons.barcode_reader, color: COLOR_ACTION),
iconSize: iconSize, iconSize: iconSize,
onPressed: () { onPressed: () {
if (InvenTreeAPI().checkConnection()) { if (InvenTreeAPI().checkConnection()) {

View File

@ -577,8 +577,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
subtitle: Text("${widget.item.partDescription}"), subtitle: Text("${widget.item.partDescription}"),
leading: InvenTreeAPI().getThumbnail(widget.item.partImage), leading: InvenTreeAPI().getThumbnail(widget.item.partImage),
trailing: Text( trailing: Text(
api.StockStatus.label(widget.item.status), widget.item.quantityString(),
style: TextStyle( style: TextStyle(
fontSize: 20,
color: api.StockStatus.color(widget.item.status), color: api.StockStatus.color(widget.item.status),
) )
), ),
@ -615,6 +616,41 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
return tiles; return tiles;
} }
// Location information
if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) {
tiles.add(
ListTile(
title: Text(L10().stockLocation),
subtitle: Text("${widget.item.locationPathString}"),
leading: FaIcon(
FontAwesomeIcons.locationDot,
color: COLOR_ACTION,
),
onTap: () async {
if (widget.item.locationId > 0) {
showLoadingOverlay(context);
var loc = await InvenTreeStockLocation().get(widget.item.locationId);
hideLoadingOverlay();
if (loc is InvenTreeStockLocation) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LocationDisplayWidget(loc)));
}
}
},
),
);
} else {
tiles.add(
ListTile(
title: Text(L10().stockLocation),
leading: FaIcon(FontAwesomeIcons.locationDot),
subtitle: Text(L10().locationNotSet),
)
);
}
// Quantity information // Quantity information
if (widget.item.isSerialized()) { if (widget.item.isSerialized()) {
tiles.add( tiles.add(
@ -634,40 +670,19 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
); );
} }
// Location information // Stock item status information
if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) { tiles.add(
tiles.add( ListTile(
ListTile( title: Text(L10().status),
title: Text(L10().stockLocation), leading: FaIcon(FontAwesomeIcons.circleInfo),
subtitle: Text("${widget.item.locationPathString}"), trailing: Text(
leading: FaIcon( api.StockStatus.label(widget.item.status),
FontAwesomeIcons.locationDot, style: TextStyle(
color: COLOR_ACTION, color: api.StockStatus.color(widget.item.status),
),
onTap: () async {
if (widget.item.locationId > 0) {
showLoadingOverlay(context);
var loc = await InvenTreeStockLocation().get(widget.item.locationId);
hideLoadingOverlay();
if (loc is InvenTreeStockLocation) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LocationDisplayWidget(loc)));
}
}
},
),
);
} else {
tiles.add(
ListTile(
title: Text(L10().stockLocation),
leading: FaIcon(FontAwesomeIcons.locationDot),
subtitle: Text(L10().locationNotSet),
) )
); )
} )
);
// Supplier part information (if available) // Supplier part information (if available)
if (widget.item.supplierPartId > 0) { if (widget.item.supplierPartId > 0) {
@ -676,7 +691,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
title: Text(L10().supplierPart), title: Text(L10().supplierPart),
subtitle: Text(widget.item.supplierSKU), subtitle: Text(widget.item.supplierSKU),
leading: FaIcon(FontAwesomeIcons.building, color: COLOR_ACTION), leading: FaIcon(FontAwesomeIcons.building, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage), trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage, hideIfNull: true),
onTap: () async { onTap: () async {
showLoadingOverlay(context); showLoadingOverlay(context);
var sp = await InvenTreeSupplierPart().get( var sp = await InvenTreeSupplierPart().get(

View File

@ -79,24 +79,37 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
}; };
@override @override
Map<String, Map<String, dynamic>> get filterOptions => { Map<String, Map<String, dynamic>> get filterOptions {
"in_stock": { Map<String, Map<String, dynamic>> filters = {
"default": true, "in_stock": {
"label": L10().filterInStock, "default": true,
"help_text": L10().filterInStockDetail, "label": L10().filterInStock,
"tristate": true, "help_text": L10().filterInStockDetail,
}, "tristate": true,
"cascade": { },
"default": false, "cascade": {
"label": L10().includeSublocations, "default": false,
"help_text": L10().includeSublocationsDetail, "label": L10().includeSublocations,
"tristate": false, "help_text": L10().includeSublocationsDetail,
}, "tristate": false,
"serialized": { },
"label": L10().filterSerialized, "serialized": {
"help_text": L10().filterSerializedDetail, "label": L10().filterSerialized,
"help_text": L10().filterSerializedDetail,
},
"status": {
"label": L10().status,
"help_text": L10().statusCode,
"choices": InvenTreeAPI().StockStatus.choices,
}
};
if (!InvenTreeAPI().supportsStatusLabelEndpoints) {
filters.remove("status");
} }
};
return filters;
}
@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 {
@ -125,6 +138,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
trailing: Text("${item.displayQuantity}", trailing: Text("${item.displayQuantity}",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16,
color: InvenTreeAPI().StockStatus.color(item.status), color: InvenTreeAPI().StockStatus.color(item.status),
), ),
), ),

View File

@ -47,25 +47,5 @@ void main() {
assert(await InvenTreeSettingsManager().getBool("chicken", true) == false); assert(await InvenTreeSettingsManager().getBool("chicken", true) == false);
}); });
test("Tri State", () async {
// Tests for tristate values
await InvenTreeSettingsManager().removeValue("dog");
// Use default values when a setting does not exist
for (bool? value in [true, false, null]) {
assert(await InvenTreeSettingsManager().getTriState("dog", value) == value);
}
// Explicitly set to a value
for (bool? value in [true, false, null]) {
await InvenTreeSettingsManager().setValue("dog", value);
assert(await InvenTreeSettingsManager().getTriState("dog", true) == value);
assert(await InvenTreeSettingsManager().getTriState("dog", false) == value);
assert(await InvenTreeSettingsManager().getTriState("dog", null) == value);
}
});
}); });
} }