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:
parent
8076887e39
commit
e9eb84eace
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,6 +13,7 @@ coverage/*
|
||||
|
||||
# This file is auto-generated as part of the CI process
|
||||
test/coverage_helper_test.dart
|
||||
InvenTreeSettings.db
|
||||
|
||||
# Sentry API key
|
||||
lib/dsn.dart
|
||||
|
@ -1,6 +1,8 @@
|
||||
### -
|
||||
---
|
||||
|
||||
- Edit part parameters from within the app
|
||||
- Increase visibility of stock quantity in widgets
|
||||
- Improved filters for stock list
|
||||
|
||||
### 0.12.2 - June 2023
|
||||
|
@ -241,6 +241,7 @@
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
|
15
lib/api.dart
15
lib/api.dart
@ -1332,7 +1332,20 @@ class InvenTreeAPI {
|
||||
|
||||
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,
|
||||
|
@ -275,6 +275,7 @@ class APIFormField {
|
||||
|
||||
// Construct a widget for this input
|
||||
Widget constructField(BuildContext context) {
|
||||
|
||||
switch (type) {
|
||||
case "string":
|
||||
case "url":
|
||||
@ -696,6 +697,14 @@ class APIFormField {
|
||||
// Construct a string input element
|
||||
Widget _constructString() {
|
||||
|
||||
if (readOnly) {
|
||||
return ListTile(
|
||||
title: Text(label),
|
||||
subtitle: Text(helpText),
|
||||
trailing: Text(value.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: required ? label + "*" : label,
|
||||
@ -724,12 +733,21 @@ class APIFormField {
|
||||
// Construct a boolean input element
|
||||
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(
|
||||
label: label,
|
||||
labelStyle: _labelStyle(),
|
||||
helperText: helpText,
|
||||
helperStyle: _helperStyle(),
|
||||
initial: value as bool?,
|
||||
initial: initial_value,
|
||||
tristate: (getParameter("tristate") ?? false) as bool,
|
||||
onSaved: (val) {
|
||||
data["value"] = val;
|
||||
@ -1262,6 +1280,10 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
|
||||
for (var field in widget.fields) {
|
||||
|
||||
if (field.readOnly) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.isSimple) {
|
||||
// Simple top-level field data
|
||||
data[field.name] = field.data["value"];
|
||||
|
@ -16,6 +16,6 @@ Color get COLOR_ACTION {
|
||||
}
|
||||
|
||||
const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
|
||||
const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1);
|
||||
const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1);
|
||||
const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1);
|
||||
const Color COLOR_DANGER = Color.fromRGBO(200, 50, 75, 1);
|
||||
const Color COLOR_SUCCESS = Color.fromRGBO(100, 200, 75, 1);
|
||||
const Color COLOR_PROGRESS = Color.fromRGBO(50, 100, 200, 1);
|
@ -133,12 +133,29 @@ class InvenTreePartParameter extends InvenTreeModel {
|
||||
@override
|
||||
String get URL => "part/parameter/";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["part"];
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartParameter.fromJson(json);
|
||||
|
||||
@override
|
||||
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
|
||||
@ -160,7 +177,11 @@ class InvenTreePartParameter extends InvenTreeModel {
|
||||
return v;
|
||||
}
|
||||
|
||||
bool get as_bool => value.toLowerCase() == "true";
|
||||
|
||||
String get units => getString("units", subKey: "template_detail");
|
||||
|
||||
bool get is_checkbox => getBool("checkbox", subKey: "template_detail", backup: false);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -23,6 +23,26 @@ class InvenTreeStatusCode {
|
||||
// Internal status code data loaded from server
|
||||
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
|
||||
Future<void> load({bool forceReload = false}) async {
|
||||
|
||||
|
@ -399,7 +399,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
double get quantity => getDouble("quantity");
|
||||
|
||||
String quantityString({bool includeUnits = false}){
|
||||
String quantityString({bool includeUnits = true}){
|
||||
|
||||
String q = "";
|
||||
|
||||
@ -467,7 +467,13 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
if (serialNumber.isNotEmpty) {
|
||||
return "SN: $serialNumber";
|
||||
} else {
|
||||
return simpleNumberString(quantity);
|
||||
String q = simpleNumberString(quantity);
|
||||
|
||||
if (units.isNotEmpty) {
|
||||
q += " ${units}";
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -278,6 +278,9 @@
|
||||
"editNotes": "Edit Notes",
|
||||
"@editNotes": {},
|
||||
|
||||
"editParameter": "Edit Parameter",
|
||||
"@editParameter": {},
|
||||
|
||||
"editPart": "Edit Part",
|
||||
"@editPart": {
|
||||
"description": "edit part"
|
||||
|
@ -126,7 +126,12 @@ class InvenTreeSettingsManager {
|
||||
|
||||
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) {
|
||||
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
|
||||
Future<void> setValue(String key, dynamic value) async {
|
||||
|
||||
// Encode null values as strings
|
||||
value ??= "null";
|
||||
value ??= "__null__";
|
||||
|
||||
await store.record(key).put(await _db, value);
|
||||
}
|
||||
|
@ -154,8 +154,8 @@ class CheckBoxField extends FormField<bool> {
|
||||
onSaved: onSaved,
|
||||
initialValue: initial,
|
||||
builder: (FormFieldState<bool> state) {
|
||||
|
||||
return CheckboxListTile(
|
||||
//dense: state.hasError,
|
||||
title: label != null ? Text(label, style: labelStyle) : null,
|
||||
value: state.value,
|
||||
tristate: tristate,
|
||||
|
@ -38,44 +38,39 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
// Override in implementing class
|
||||
String get prefix => "prefix_";
|
||||
|
||||
// Return a map of boolean filtering options available for this list
|
||||
// Should be overridden by an implementing subclass
|
||||
Map<String, Map<String, dynamic>> get filterOptions => {};
|
||||
|
||||
// Return the boolean value of a particular boolean filter
|
||||
Future<bool?> getBooleanFilterValue(String key) async {
|
||||
key = "${prefix}bool_${key}";
|
||||
Future<dynamic> getFilterValue(String key) async {
|
||||
key = "${prefix}filter_${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;
|
||||
}
|
||||
|
||||
// Set the boolean value of a particular boolean filter
|
||||
Future<void> setBooleanFilterValue(String key, bool? value) async {
|
||||
key = "${prefix}bool_${key}";
|
||||
Future<void> setFilterValue(String key, dynamic value) async {
|
||||
key = "${prefix}filter_${key}";
|
||||
await InvenTreeSettingsManager().setValue(key, value);
|
||||
}
|
||||
|
||||
// Construct the boolean filter options for this list
|
||||
Future<Map<String, String>> constructBooleanFilters() async {
|
||||
Future<Map<String, String>> constructFilters() async {
|
||||
|
||||
Map<String, String> f = {};
|
||||
|
||||
for (String k in filterOptions.keys) {
|
||||
bool? value = await getBooleanFilterValue(k);
|
||||
dynamic value = await getFilterValue(k);
|
||||
|
||||
if (value is bool) {
|
||||
f[k] = value ? "true" : "false";
|
||||
// Skip null values
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
f[k] = value.toString();
|
||||
}
|
||||
|
||||
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) {
|
||||
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? help_text = opts["help_text"] as String?;
|
||||
|
||||
List<dynamic> choices = (opts["choices"] ?? []) as List<dynamic>;
|
||||
|
||||
bool tristate = (opts["tristate"] ?? true) as bool;
|
||||
|
||||
bool? v = await getBooleanFilterValue(key);
|
||||
dynamic v = await getFilterValue(key);
|
||||
|
||||
// Prevent null value if not tristate
|
||||
if (!tristate && v == null) {
|
||||
v = false;
|
||||
}
|
||||
|
||||
// Add in the particular field
|
||||
fields[key] = {
|
||||
Map<String, dynamic> filter = {
|
||||
"type": "boolean",
|
||||
"display_name": label,
|
||||
"label": label,
|
||||
@ -190,6 +186,16 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
"value": v,
|
||||
"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
|
||||
@ -211,16 +217,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
|
||||
// Save boolean fields
|
||||
for (String key in filterOptions.keys) {
|
||||
|
||||
bool? v;
|
||||
|
||||
dynamic value = data[key];
|
||||
|
||||
if (value is bool) {
|
||||
v = value;
|
||||
}
|
||||
|
||||
await setBooleanFilterValue(key, v);
|
||||
await setFilterValue(key, data[key]);
|
||||
}
|
||||
|
||||
// Refresh data from the server
|
||||
@ -293,7 +290,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
params["ordering"] = o;
|
||||
}
|
||||
|
||||
Map<String, String> f = await constructBooleanFilters();
|
||||
Map<String, String> f = await constructFilters();
|
||||
|
||||
if (f.isNotEmpty) {
|
||||
params.addAll(f);
|
||||
@ -348,6 +345,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
void updateSearchTerm() {
|
||||
searchTerm = searchController.text;
|
||||
_pagingController.refresh();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to construct a single paginated item
|
||||
@ -409,19 +410,19 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
*/
|
||||
Widget buildSearchInput(BuildContext context) {
|
||||
return ListTile(
|
||||
trailing: orderingOptions.isEmpty ? null : GestureDetector(
|
||||
child: FaIcon(FontAwesomeIcons.sort, color: COLOR_ACTION),
|
||||
leading: orderingOptions.isEmpty ? null : GestureDetector(
|
||||
child: Icon(Icons.filter_list, color: COLOR_ACTION, size: 32),
|
||||
onTap: () async {
|
||||
_saveOrderingOptions(context);
|
||||
},
|
||||
),
|
||||
leading: GestureDetector(
|
||||
trailing: GestureDetector(
|
||||
child: FaIcon(
|
||||
searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft,
|
||||
color: searchController.text.isNotEmpty ? COLOR_DANGER : null,
|
||||
color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_ACTION,
|
||||
),
|
||||
onTap: () {
|
||||
if (searchController.text.isEmpty) {
|
||||
if (searchController.text.isNotEmpty) {
|
||||
searchController.clear();
|
||||
}
|
||||
updateSearchTerm();
|
||||
|
@ -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) {
|
||||
|
||||
part.editForm(
|
||||
@ -259,13 +245,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
child: ListTile(
|
||||
title: Text("${part.fullname}"),
|
||||
subtitle: Text("${part.description}"),
|
||||
trailing: IconButton(
|
||||
icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star,
|
||||
color: part.starred ? Colors.yellowAccent : null,
|
||||
),
|
||||
onPressed: () {
|
||||
_toggleStar(context);
|
||||
},
|
||||
trailing: Text(
|
||||
part.stockString(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
)
|
||||
),
|
||||
leading: GestureDetector(
|
||||
child: api.getImage(part.thumbnail),
|
||||
|
@ -133,7 +133,13 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
|
||||
return ListTile(
|
||||
title: Text(part.fullname),
|
||||
subtitle: Text(part.description),
|
||||
trailing: Text(part.stockString()),
|
||||
trailing: Text(
|
||||
part.stockString(),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold
|
||||
)
|
||||
),
|
||||
leading: InvenTreeAPI().getThumbnail(part.thumbnail),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
||||
|
@ -4,6 +4,7 @@ import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/paginator.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
|
||||
/*
|
||||
@ -75,7 +76,7 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
||||
|
||||
@override
|
||||
Map<String, String> get orderingOptions => {
|
||||
// TODO
|
||||
|
||||
};
|
||||
|
||||
@override
|
||||
@ -91,6 +92,22 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
||||
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
|
||||
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
||||
|
||||
@ -99,7 +116,28 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
||||
return ListTile(
|
||||
title: Text(parameter.name),
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ mixin BaseWidgetProperties {
|
||||
*/
|
||||
BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
|
||||
|
||||
const double iconSize = 32;
|
||||
const double iconSize = 40;
|
||||
|
||||
List<Widget> icons = [
|
||||
IconButton(
|
||||
@ -98,7 +98,7 @@ mixin BaseWidgetProperties {
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.qr_code_scanner, color: COLOR_ACTION),
|
||||
icon: Icon(Icons.barcode_reader, color: COLOR_ACTION),
|
||||
iconSize: iconSize,
|
||||
onPressed: () {
|
||||
if (InvenTreeAPI().checkConnection()) {
|
||||
|
@ -577,8 +577,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
subtitle: Text("${widget.item.partDescription}"),
|
||||
leading: InvenTreeAPI().getThumbnail(widget.item.partImage),
|
||||
trailing: Text(
|
||||
api.StockStatus.label(widget.item.status),
|
||||
widget.item.quantityString(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: api.StockStatus.color(widget.item.status),
|
||||
)
|
||||
),
|
||||
@ -615,25 +616,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
return tiles;
|
||||
}
|
||||
|
||||
// Quantity information
|
||||
if (widget.item.isSerialized()) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().serialNumber),
|
||||
leading: FaIcon(FontAwesomeIcons.hashtag),
|
||||
trailing: Text("${widget.item.serialNumber}"),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: widget.item.allocated > 0 ? Text(L10().quantityAvailable) : Text(L10().quantity),
|
||||
leading: FaIcon(FontAwesomeIcons.cubes),
|
||||
trailing: Text("${widget.item.quantityString()}"),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Location information
|
||||
if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) {
|
||||
tiles.add(
|
||||
@ -669,6 +651,39 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
// Quantity information
|
||||
if (widget.item.isSerialized()) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().serialNumber),
|
||||
leading: FaIcon(FontAwesomeIcons.hashtag),
|
||||
trailing: Text("${widget.item.serialNumber}"),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: widget.item.allocated > 0 ? Text(L10().quantityAvailable) : Text(L10().quantity),
|
||||
leading: FaIcon(FontAwesomeIcons.cubes),
|
||||
trailing: Text("${widget.item.quantityString()}"),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Stock item status information
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().status),
|
||||
leading: FaIcon(FontAwesomeIcons.circleInfo),
|
||||
trailing: Text(
|
||||
api.StockStatus.label(widget.item.status),
|
||||
style: TextStyle(
|
||||
color: api.StockStatus.color(widget.item.status),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Supplier part information (if available)
|
||||
if (widget.item.supplierPartId > 0) {
|
||||
tiles.add(
|
||||
@ -676,7 +691,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
title: Text(L10().supplierPart),
|
||||
subtitle: Text(widget.item.supplierSKU),
|
||||
leading: FaIcon(FontAwesomeIcons.building, color: COLOR_ACTION),
|
||||
trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage),
|
||||
trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage, hideIfNull: true),
|
||||
onTap: () async {
|
||||
showLoadingOverlay(context);
|
||||
var sp = await InvenTreeSupplierPart().get(
|
||||
|
@ -79,7 +79,8 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
||||
};
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> get filterOptions => {
|
||||
Map<String, Map<String, dynamic>> get filterOptions {
|
||||
Map<String, Map<String, dynamic>> filters = {
|
||||
"in_stock": {
|
||||
"default": true,
|
||||
"label": L10().filterInStock,
|
||||
@ -95,9 +96,21 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
||||
"serialized": {
|
||||
"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
|
||||
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}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: InvenTreeAPI().StockStatus.color(item.status),
|
||||
),
|
||||
),
|
||||
|
@ -47,25 +47,5 @@ void main() {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user