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

Merge branch 'snackbars'

This commit is contained in:
Oliver Walters 2021-02-11 00:25:51 +11:00
commit 3ec7ed217e
11 changed files with 310 additions and 117 deletions

View File

@ -1,4 +1,5 @@
import 'package:InvenTree/inventree/part.dart'; import 'package:InvenTree/inventree/part.dart';
import 'package:InvenTree/widget/dialogs.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'model.dart'; import 'model.dart';
@ -323,6 +324,15 @@ class InvenTreeStockItem extends InvenTreeModel {
double get quantity => double.tryParse(jsondata['quantity'].toString() ?? '0'); double get quantity => double.tryParse(jsondata['quantity'].toString() ?? '0');
String get quantityString {
if (quantity.toInt() == quantity) {
return quantity.toInt().toString();
} else {
return quantity.toString();
}
}
int get locationId => jsondata['location'] as int ?? -1; int get locationId => jsondata['location'] as int ?? -1;
bool isSerialized() => serialNumber != null && quantity.toInt() == 1; bool isSerialized() => serialNumber != null && quantity.toInt() == 1;
@ -382,51 +392,81 @@ class InvenTreeStockItem extends InvenTreeModel {
return item; return item;
} }
Future<http.Response> countStock(double quan, {String notes}) async { /*
* Perform stocktake action:
*
* - Add
* - Remove
* - Count
*/
Future<bool> adjustStock(BuildContext context, String endpoint, double q, {String notes}) async {
// Cannot 'count' a serialized StockItem // Serialized stock cannot be adjusted
if (isSerialized()) { if (isSerialized()) {
return null; return false;
} }
// Cannot count negative stock // Cannot handle negative stock
if (quan < 0) { if (q < 0) {
return null; return false;
} }
return api.post("/stock/count/", body: { var response = await api.post(
"item": { endpoint,
"pk": "${pk}", body: {
"quantity": "${quan}", "item": {
}, "pk": "${pk}",
"notes": notes ?? '', "quantity": "${q}",
},
"notes": notes ?? '',
}).timeout(Duration(seconds: 10)).catchError((error) {
if (error is TimeoutException) {
showTimeoutError(context);
} else if (error is SocketException) {
showServerError(
context,
I18N.of(context).connectionRefused,
error.toString()
);
} else {
// Re-throw the error, let sentry handle it!
throw error;
}
// Null response if error
return null;
}); });
if (response == null) return false;
if (response.statusCode != 200) {
showStatusCodeError(context, response.statusCode);
return false;
}
// Stock adjustment succeeded!
return true;
} }
Future<http.Response> addStock(double quan, {String notes}) async { Future<bool> countStock(BuildContext context, double q, {String notes}) async {
if (isSerialized() || quan <= 0) return null; final bool result = await adjustStock(context, "/stock/count", q, notes: notes);
return api.post("/stock/add/", body: { return result;
"item": {
"pk": "${pk}",
"quantity": "${quan}",
},
"notes": notes ?? '',
});
} }
Future<http.Response> removeStock(double quan, {String notes}) async { Future<bool> addStock(BuildContext context, double q, {String notes}) async {
if (isSerialized() || quan <= 0) return null; final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
return api.post("/stock/remove/", body: { return result;
"item": { }
"pk": "${pk}",
"quantity": "${quan}", Future<bool> removeStock(BuildContext context, double q, {String notes}) async {
},
"notes": notes ?? '', final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
});
return result;
} }
Future<http.Response> transferStock(int location, {double quantity, String notes}) async { Future<http.Response> transferStock(int location, {double quantity, String notes}) async {

@ -1 +1 @@
Subproject commit 249e4964a08b79e53df7e1ea18b051de0d307905 Subproject commit ed3bd59b15b2c69b9a21649a0e0507efd811c1a1

View File

@ -5,6 +5,9 @@ import 'package:InvenTree/preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:InvenTree/widget/fields.dart';
import 'package:InvenTree/widget/dialogs.dart';
import 'package:InvenTree/widget/snacks.dart';
import 'package:InvenTree/widget/part_detail.dart'; import 'package:InvenTree/widget/part_detail.dart';
import 'package:InvenTree/widget/drawer.dart'; import 'package:InvenTree/widget/drawer.dart';
import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:InvenTree/widget/refreshable_state.dart';
@ -28,6 +31,8 @@ class CategoryDisplayWidget extends StatefulWidget {
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
final _editCategoryKey = GlobalKey<FormState>();
@override @override
String getAppBarTitle(BuildContext context) => I18N.of(context).partCategory; String getAppBarTitle(BuildContext context) => I18N.of(context).partCategory;
@ -37,11 +42,54 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.edit), icon: FaIcon(FontAwesomeIcons.edit),
tooltip: I18N.of(context).edit, tooltip: I18N.of(context).edit,
onPressed: null, onPressed: _editCategoryDialog,
) )
]; ];
} }
void _editCategory(Map<String, String> values) async {
final bool result = await category.update(context, values: values);
showSnackIcon(
refreshableKey,
result ? "Category edited" : "Category editing failed",
success: result
);
refresh();
}
void _editCategoryDialog() {
var _name;
var _description;
showFormDialog(
context,
I18N.of(context).editCategory,
key: _editCategoryKey,
callback: () {
_editCategory({
"name": _name,
"description": _description
});
},
fields: <Widget>[
StringField(
label: I18N.of(context).name,
initial: category.name,
onSaved: (value) => _name = value
),
StringField(
label: I18N.of(context).description,
initial: category.description,
onSaved: (value) => _description = value
)
]
);
}
_CategoryDisplayState(this.category) {} _CategoryDisplayState(this.category) {}
// The local InvenTreePartCategory object // The local InvenTreePartCategory object
@ -61,6 +109,11 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
int pk = category?.pk ?? -1; int pk = category?.pk ?? -1;
// Update the category
if (category != null) {
await category.reload(context);
}
// Request a list of sub-categories under this one // Request a list of sub-categories under this one
await InvenTreePartCategory().list(context, filters: {"parent": "$pk"}).then((var cats) { await InvenTreePartCategory().list(context, filters: {"parent": "$pk"}).then((var cats) {
_subcategories.clear(); _subcategories.clear();
@ -266,7 +319,7 @@ class PartList extends StatelessWidget {
} }
return ListTile( return ListTile(
title: Text("${part.name}"), title: Text(part.fullname),
subtitle: Text("${part.description}"), subtitle: Text("${part.description}"),
trailing: Text("${part.inStockString}"), trailing: Text("${part.inStockString}"),
leading: InvenTreeAPI().getImage( leading: InvenTreeAPI().getImage(

View File

@ -161,7 +161,39 @@ void hideProgressDialog(BuildContext context) {
Navigator.pop(context); Navigator.pop(context);
} }
void showFormDialog(BuildContext context, String title, {GlobalKey<FormState> key, List<Widget> fields, List<Widget> actions}) { void showFormDialog(BuildContext context, String title, {GlobalKey<FormState> key, List<Widget> fields, List<Widget> actions, Function callback}) {
// Undefined actions = OK + Cancel
if (actions == null) {
actions = <Widget>[
FlatButton(
child: Text(I18N.of(context).cancel),
onPressed: () {
// Close the form
Navigator.pop(context);
}
),
FlatButton(
child: Text(I18N.of(context).save),
onPressed: () {
if (key.currentState.validate()) {
key.currentState.save();
// Close the dialog
Navigator.pop(context);
// Callback
if (callback != null) {
callback();
}
}
}
)
];
}
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {

View File

@ -1,13 +1,18 @@
import 'package:InvenTree/api.dart'; import 'package:InvenTree/api.dart';
import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/inventree/stock.dart';
import 'package:InvenTree/preferences.dart'; import 'package:InvenTree/preferences.dart';
import 'package:InvenTree/widget/refreshable_state.dart';
import 'package:InvenTree/widget/fields.dart';
import 'package:InvenTree/widget/dialogs.dart';
import 'package:InvenTree/widget/snacks.dart';
import 'package:InvenTree/widget/stock_detail.dart'; import 'package:InvenTree/widget/stock_detail.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:InvenTree/widget/refreshable_state.dart';
class LocationDisplayWidget extends StatefulWidget { class LocationDisplayWidget extends StatefulWidget {
@ -25,6 +30,8 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
final InvenTreeStockLocation location; final InvenTreeStockLocation location;
final _editLocationKey = GlobalKey<FormState>();
@override @override
String getAppBarTitle(BuildContext context) { return "Stock Location"; } String getAppBarTitle(BuildContext context) { return "Stock Location"; }
@ -33,13 +40,53 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
return <Widget>[ return <Widget>[
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.edit), icon: FaIcon(FontAwesomeIcons.edit),
tooltip: "Edit", tooltip: I18N.of(context).edit,
// TODO - Edit stock location onPressed: _editLocationDialog,
onPressed: null,
) )
]; ];
} }
void _editLocation(Map<String, String> values) async {
final bool result = await location.update(context, values: values);
showSnackIcon(
refreshableKey,
result ? "Location edited" : "Location editing failed",
success: result
);
refresh();
}
void _editLocationDialog() {
// Values which an be edited
var _name;
var _description;
showFormDialog(context, I18N.of(context).editLocation,
key: _editLocationKey,
callback: () {
_editLocation({
"name": _name,
"description": _description
});
},
fields: <Widget> [
StringField(
label: I18N.of(context).name,
initial: location.name,
onSaved: (value) => _name = value,
),
StringField(
label: I18N.of(context).description,
initial: location.description,
onSaved: (value) => _description = value,
)
]
);
}
_LocationDisplayState(this.location) {} _LocationDisplayState(this.location) {}
List<InvenTreeStockLocation> _sublocations = List<InvenTreeStockLocation>(); List<InvenTreeStockLocation> _sublocations = List<InvenTreeStockLocation>();
@ -67,6 +114,11 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
int pk = location?.pk ?? -1; int pk = location?.pk ?? -1;
// Reload location information
if (location != null) {
await location.reload(context);
}
// Request a list of sub-locations under this one // Request a list of sub-locations under this one
await InvenTreeStockLocation().list(context, filters: {"parent": "$pk"}).then((var locs) { await InvenTreeStockLocation().list(context, filters: {"parent": "$pk"}).then((var locs) {
_sublocations.clear(); _sublocations.clear();

View File

@ -1,4 +1,6 @@
import 'dart:io';
import 'package:InvenTree/widget/snacks.dart';
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';
@ -74,9 +76,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
void _savePart(Map<String, String> values) async { void _savePart(Map<String, String> values) async {
Navigator.of(context).pop(); final bool result = await part.update(context, values: values);
var response = await part.update(context, values: values); showSnackIcon(
refreshableKey,
result ? "Part edited" : "Part editing failed",
success: result
);
refresh(); refresh();
} }
@ -96,34 +102,18 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
var _name; var _name;
var _description; var _description;
var _ipn; var _ipn;
var _revision;
var _keywords; var _keywords;
showFormDialog(context, I18N.of(context).editPart, showFormDialog(context, I18N.of(context).editPart,
key: _editPartKey, key: _editPartKey,
actions: <Widget>[ callback: () {
FlatButton( _savePart({
child: Text(I18N.of(context).cancel), "name": _name,
onPressed: () { "description": _description,
Navigator.pop(context); "IPN": _ipn,
}, "keywords": _keywords
), });
FlatButton( },
child: Text(I18N.of(context).save),
onPressed: () {
if (_editPartKey.currentState.validate()) {
_editPartKey.currentState.save();
_savePart({
"name": _name,
"description": _description,
"IPN": _ipn,
"keywords": _keywords,
});
}
},
),
],
fields: <Widget>[ fields: <Widget>[
StringField( StringField(
label: I18N.of(context).name, label: I18N.of(context).name,
@ -163,7 +153,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => FullScreenWidget(part.name, part.image)) MaterialPageRoute(builder: (context) => FullScreenWidget(part.fullname, part.image))
); );
}), }),
) )

View File

@ -58,7 +58,7 @@ class _PartStockDisplayState extends RefreshableState<PartStockDetailWidget> {
title: Text(part.fullname), title: Text(part.fullname),
subtitle: Text(part.description), subtitle: Text(part.description),
leading: InvenTreeAPI().getImage(part.thumbnail), leading: InvenTreeAPI().getImage(part.thumbnail),
trailing: Text('${part.inStock}'), trailing: Text(part.inStockString),
) )
), ),
PartStockList(part.stockItems), PartStockList(part.stockItems),

View File

@ -8,6 +8,8 @@ import 'package:InvenTree/widget/drawer.dart';
abstract class RefreshableState<T extends StatefulWidget> extends State<T> { abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
final refreshableKey = GlobalKey<ScaffoldState>();
// Storage for context once "Build" is called // Storage for context once "Build" is called
BuildContext context; BuildContext context;
@ -80,6 +82,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
this.context = context; this.context = context;
return Scaffold( return Scaffold(
key: refreshableKey,
appBar: getAppBar(context), appBar: getAppBar(context),
drawer: getDrawer(context), drawer: getDrawer(context),
floatingActionButton: getFab(context), floatingActionButton: getFab(context),

36
lib/widget/snacks.dart Normal file
View File

@ -0,0 +1,36 @@
/*
* Display a snackbar with:
*
* a) Text on the left
* b) Icon on the right
*
* | Text <icon> |
*/
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
void showSnackIcon(GlobalKey<ScaffoldState> key, String text, {IconData icon, bool success}) {
// Hide the current snackbar
key.currentState.hideCurrentSnackBar();
// If icon not specified, use the success status
if (icon == null) {
icon = (success == false) ? FontAwesomeIcons.timesCircle : FontAwesomeIcons.checkCircle;
}
key.currentState.showSnackBar(
SnackBar(
content: Row(
children: [
Text(text),
Spacer(),
FaIcon(icon)
]
),
)
);
}

View File

@ -1,4 +1,5 @@
import 'dart:io';
import 'package:InvenTree/barcode.dart'; import 'package:InvenTree/barcode.dart';
import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/inventree/stock.dart';
@ -8,6 +9,7 @@ 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/refreshable_state.dart'; import 'package:InvenTree/widget/refreshable_state.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:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -22,6 +24,7 @@ import 'package:flutter_typeahead/flutter_typeahead.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:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:http/http.dart';
class StockDetailWidget extends StatefulWidget { class StockDetailWidget extends StatefulWidget {
@ -67,14 +70,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
double quantity = double.parse(_quantityController.text); double quantity = double.parse(_quantityController.text);
_quantityController.clear(); _quantityController.clear();
// Await response to prevent the button from being pressed multiple times final bool result = await item.addStock(context, quantity, notes: _notesController.text);
var response = await item.addStock(quantity, notes: _notesController.text);
_notesController.clear(); _notesController.clear();
// TODO - Handle error cases _stockUpdateMessage(result);
refresh();
// TODO - Display a snackbar here indicating the action was successful (or otherwise) refresh();
} }
void _addStockDialog() async { void _addStockDialog() async {
@ -108,20 +109,27 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
); );
} }
void _stockUpdateMessage(bool result) {
showSnackIcon(
refreshableKey,
result ? "Stock item updated" : "Stock item updated failed",
success: result
);
}
void _removeStock() async { void _removeStock() async {
Navigator.of(context).pop(); Navigator.of(context).pop();
double quantity = double.parse(_quantityController.text); double quantity = double.parse(_quantityController.text);
_quantityController.clear(); _quantityController.clear();
var response = await item.removeStock(quantity, notes: _notesController.text); final bool result = await item.removeStock(context, quantity, notes: _notesController.text);
_notesController.clear();
// TODO - Handle error cases _stockUpdateMessage(result);
refresh(); refresh();
// TODO - Display a snackbar here indicating the action was successful (or otherwise)
} }
void _removeStockDialog() { void _removeStockDialog() {
@ -163,19 +171,16 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
double quantity = double.parse(_quantityController.text); double quantity = double.parse(_quantityController.text);
_quantityController.clear(); _quantityController.clear();
var response = await item.countStock(quantity, notes: _notesController.text); final bool result = await item.countStock(context, quantity, notes: _notesController.text);
_notesController.clear();
// TODO - Handle error cases, timeout, etc _stockUpdateMessage(result);
refresh(); refresh();
// TODO - Display a snackbar here indicating the action was successful (or otherwise)
} }
void _countStockDialog() async { void _countStockDialog() async {
_quantityController.text = item.quantity.toString(); _quantityController.text = item.quantityString;
_notesController.clear(); _notesController.clear();
showFormDialog(context, I18N.of(context).countStock, showFormDialog(context, I18N.of(context).countStock,
@ -191,7 +196,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
fields: <Widget> [ fields: <Widget> [
QuantityField( QuantityField(
label: I18N.of(context).countStock, label: I18N.of(context).countStock,
hint: "${item.quantity}", hint: "${item.quantityString}",
controller: _quantityController, controller: _quantityController,
), ),
TextFormField( TextFormField(
@ -230,7 +235,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
InvenTreeStockLocation selectedLocation; InvenTreeStockLocation selectedLocation;
_quantityController.text = "${item.quantity}"; _quantityController.text = "${item.quantityString}";
showFormDialog(context, I18N.of(context).transferStock, showFormDialog(context, I18N.of(context).transferStock,
key: _moveStockKey, key: _moveStockKey,
@ -382,7 +387,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
ListTile( ListTile(
title: Text(I18N.of(context).quantity), title: Text(I18N.of(context).quantity),
leading: FaIcon(FontAwesomeIcons.cubes), leading: FaIcon(FontAwesomeIcons.cubes),
trailing: Text("${item.quantity}"), trailing: Text("${item.quantityString}"),
) )
); );
} }

View File

@ -4,6 +4,7 @@ import 'package:InvenTree/inventree/model.dart';
import 'package:InvenTree/api.dart'; import 'package:InvenTree/api.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/snacks.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -46,24 +47,20 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
void uploadTestResult(String name, bool result, String value, String notes, File attachment) async { void uploadTestResult(String name, bool result, String value, String notes, File attachment) async {
item.uploadTestResult( final success = await item.uploadTestResult(
context, context, name, result,
name,
result,
value: value, value: value,
notes: notes, notes: notes,
attachment: attachment attachment: attachment
).then((bool success) { );
if (success) {
// TODO - Show a SnackBar here! showSnackIcon(
refresh(); refreshableKey,
} else { success ? "Test result uploaded" : "Could not upload test result",
showErrorDialog( success: success
context, );
I18N.of(context).error,
"Could not upload test result to server"); refresh();
}
});
} }
void addTestResult({String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async { void addTestResult({String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
@ -76,24 +73,9 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
showFormDialog(context, "Add Test Data", showFormDialog(context, "Add Test Data",
key: _addResultKey, key: _addResultKey,
actions: <Widget>[ callback: () {
FlatButton( uploadTestResult(_name, _result, _value, _notes, _attachment);
child: Text(I18N.of(context).cancel), },
onPressed: () {
Navigator.pop(context);
},
),
FlatButton(
child: Text(I18N.of(context).save),
onPressed: () {
if (_addResultKey.currentState.validate()) {
_addResultKey.currentState.save();
Navigator.pop(context);
uploadTestResult(_name, _result, _value, _notes, _attachment);
}
},
)
],
fields: <Widget>[ fields: <Widget>[
StringField( StringField(
label: "Test Name", label: "Test Name",