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

Merge pull request #19 from SchrodingersGat/stock-item-page

Improve view for StockItem
This commit is contained in:
Oliver 2020-04-06 22:52:24 +10:00 committed by GitHub
commit c7fbe99f53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 459 additions and 130 deletions

View File

@ -72,8 +72,46 @@ class InvenTreeAPI {
// Authentication token (initially empty, must be requested)
String _token = "";
bool isConnected() {
return _token.isNotEmpty;
}
/*
* Check server connection and display messages if not connected.
* Useful as a precursor check before performing operations.
*/
bool checkConnection(BuildContext context) {
// Firstly, is the server connected?
if (!isConnected()) {
showDialog(
context: context,
child: new SimpleDialog(
title: new Text("Not Connected"),
children: <Widget> [
ListTile(
title: Text("Server not connected"),
)
]
)
);
return false;
}
// Is the server version too old?
// TODO
// Finally
return true;
}
// Server instance information
String instance = '';
// Server version information
String _version;
String _version = '';
// Getter for server version information
String get version => _version;
@ -174,6 +212,9 @@ class InvenTreeAPI {
_version = data["version"];
// Record the instance name of the server
instance = data['instance'] ?? '';
// Request token from the server if we do not already have one
if (_token.isNotEmpty) {
print("Already have token - $_token");

View File

@ -74,8 +74,13 @@ class InvenTreeModel {
}
*/
Map<String, String> defaultListFilters() { return Map<String, String>(); }
// A map of "default" headers to use when performing a GET request
Map<String, String> defaultGetFilters() { return Map<String, String>(); }
// Return the detail view for the associated pk
Future<InvenTreeModel> get(int pk) async {
Future<InvenTreeModel> get(int pk, {Map<String, String> filters}) async {
// TODO - Add "timeout"
// TODO - Add error catching
@ -86,7 +91,18 @@ class InvenTreeModel {
addr += "/";
}
var response = await InvenTreeAPI().get(addr);
var params = defaultGetFilters();
if (filters != null) {
// Override any default values
for (String key in filters.keys) {
params[key] = filters[key];
}
}
print("GET: $addr ${params.toString()}");
var response = await InvenTreeAPI().get(addr, params: params);
if (response.statusCode != 200) {
print("Error retrieving data");
@ -105,12 +121,20 @@ class InvenTreeModel {
filters = {};
}
print("Listing endpoint: $URL");
var params = defaultListFilters();
if (filters != null) {
for (String key in filters.keys) {
params[key] = filters[key];
}
}
print("LIST: $URL ${params.toString()}");
// TODO - Add "timeout"
// TODO - Add error catching
var response = await InvenTreeAPI().get(URL, params:filters);
var response = await InvenTreeAPI().get(URL, params:params);
// A list of "InvenTreeModel" items
List<InvenTreeModel> results = new List<InvenTreeModel>();

View File

@ -12,6 +12,15 @@ class InvenTreePartCategory extends InvenTreeModel {
@override
String URL = "part/category/";
@override
Map<String, String> defaultListFilters() {
var filters = new Map<String, String>();
filters["active"] = "true";
return filters;
}
String get pathstring => jsondata['pathstring'] ?? '';
String get parentpathstring {

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'model.dart';
import 'package:InvenTree/api.dart';
@ -6,15 +8,72 @@ class InvenTreeStockItem extends InvenTreeModel {
@override
String URL = "stock/";
@override
Map<String, String> defaultGetFilters() {
var headers = new Map<String, String>();
headers["part_detail"] = "true";
headers["location_detail"] = "true";
headers["supplier_detail"] = "true";
return headers;
}
InvenTreeStockItem() : super();
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
// TODO
}
String get partName => jsondata['part__name'] as String ?? '';
int get partId => jsondata['part'] ?? -1;
String get partDescription => jsondata['part__description'] as String ?? '';
int get trackingItemCount => jsondata['tracking_items'] as int ?? 0;
String get partName {
String nm = '';
// Use the detailed part information as priority
if (jsondata.containsKey('part_detail')) {
nm = jsondata['part_detail']['full_name'] ?? '';
}
if (nm.isEmpty) {
nm = jsondata['part__name'] ?? '';
}
return nm;
}
String get partDescription {
String desc = '';
// Use the detailed part description as priority
if (jsondata.containsKey('part_detail')) {
desc = jsondata['part_detail']['description'] ?? '';
}
if (desc.isEmpty) {
desc = jsondata['part__description'] ?? '';
}
return desc;
}
String get partImage {
String img = '';
if (jsondata.containsKey('part_detail')) {
img = jsondata['part_detail']['thumbnail'] ?? '';
}
if (img.isEmpty) {
img = jsondata['part__thumbnail'] ?? '';
}
return img;
}
String get partThumbnail {
String thumb = jsondata['part__thumbnail'] as String ?? '';
@ -24,12 +83,58 @@ class InvenTreeStockItem extends InvenTreeModel {
return thumb;
}
int get supplierPartId => jsondata['supplier_part'] as int ?? -1;
String get supplierImage {
String thumb = '';
if (jsondata.containsKey("supplier_detail")) {
thumb = jsondata['supplier_detail']['supplier_logo'] ?? '';
}
return thumb;
}
String get supplierName {
String sname = '';
if (jsondata.containsKey("supplier_detail")) {
sname = jsondata["supplier_detail"]["supplier_name"] ?? '';
}
return sname;
}
String get supplierSKU {
String sku = '';
if (jsondata.containsKey("supplier_detail")) {
sku = jsondata["supplier_detail"]["SKU"] ?? '';
}
return sku;
}
int get serialNumber => jsondata['serial'] as int ?? null;
double get quantity => jsondata['quantity'] as double ?? 0.0;
double get quantity => double.tryParse(jsondata['quantity'].toString() ?? '0');
int get locationId => jsondata['location'] as int ?? -1;
String get locationName {
String loc = '';
if (jsondata.containsKey('location_detail')) {
loc = jsondata['location_detail']['name'] ?? '';
}
if (loc.isEmpty) {
loc = jsondata['location__name'] ?? '';
}
return loc;
}
String get displayQuantity {
// Display either quantity or serial number!

View File

@ -14,7 +14,7 @@ import 'barcode.dart';
import 'dart:convert';
import 'settings.dart';
import 'settings/settings.dart';
import 'api.dart';
import 'preferences.dart';
@ -151,6 +151,13 @@ class _MyHomePageState extends State<MyHomePage> {
_serverAddress = prefs.getString("server");
// Reset the connection status variables
_serverStatus = "Connecting to server";
_serverMessage = "";
_serverConnection = false;
_serverIcon = new FaIcon(FontAwesomeIcons.spinner);
_serverStatusColor = Color.fromARGB(255, 50, 50, 250);
InvenTreeAPI().connect().then((bool result) {
if (result) {
@ -176,21 +183,33 @@ class _MyHomePageState extends State<MyHomePage> {
onConnectFailure(fault);
});
// Update widget state
setState(() {});
}
void _search() {
if (!InvenTreeAPI().checkConnection(context)) return;
// TODO
}
void _scan() {
if (!InvenTreeAPI().checkConnection(context)) return;
scanQrCode(context);
}
void _parts() {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
}
void _stock() {
if (!InvenTreeAPI().checkConnection(context)) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
}
@ -349,6 +368,11 @@ class _MyHomePageState extends State<MyHomePage> {
style: TextStyle(color: _serverStatusColor),
),
leading: _serverIcon,
onTap: () {
if (!_serverConnection) {
_checkServerConnection();
}
},
),
),
],

64
lib/settings/about.dart Normal file
View File

@ -0,0 +1,64 @@
import 'package:InvenTree/api.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:package_info/package_info.dart';
class InvenTreeAboutWidget extends StatelessWidget {
final PackageInfo info;
InvenTreeAboutWidget(this.info) : super();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("About InvenTree"),
),
body: ListView(
children: <Widget>[
ListTile(
title: Text("Server Address"),
subtitle: Text(InvenTreeAPI().baseUrl.isNotEmpty ? InvenTreeAPI().baseUrl : "Not connected"),
),
ListTile(
title: Text("Server Version"),
subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"),
),
ListTile(
title: Text("Server Instance"),
subtitle: Text(InvenTreeAPI().instance.isNotEmpty ? InvenTreeAPI().instance : "Not connected"),
),
Divider(),
ListTile(
title: Text("App Name"),
subtitle: Text("${info.appName}"),
),
ListTile(
title: Text("Package Name"),
subtitle: Text("${info.packageName}"),
),
ListTile(
title: Text("App Version"),
subtitle: Text("${info.version}"),
),
ListTile(
title: Text("Build Number"),
subtitle: Text("${info.buildNumber}")
),
Divider(),
ListTile(
title: Text("Submit Bug Report"),
subtitle: Text("https://github.com/inventree/inventree-app/issues/"),
onTap: () {
// TODO - Open the URL in an external webpage?
},
)
],
)
);
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'api.dart';
import 'preferences.dart';
import '../api.dart';
import '../preferences.dart';
class InvenTreeLoginSettingsWidget extends StatefulWidget {
@ -81,7 +81,6 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
initialValue: _server,
decoration: InputDecoration(
hintText: "127.0.0.1:8000",
labelText: "Server:Port",
),
validator: _validateServer,
onSaved: (String value) {
@ -89,7 +88,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
},
),
Divider(),
Text("Login Details"),
Text("Account Details"),
TextFormField(
initialValue: _username,
decoration: InputDecoration(

View File

@ -1,8 +1,12 @@
import 'package:InvenTree/settings/about.dart';
import 'package:InvenTree/settings/login.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:InvenTree/api.dart';
import 'login_settings.dart';
import 'login.dart';
import 'package:package_info/package_info.dart';
@ -28,12 +32,14 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
ListTile(
title: Text("Server Settings"),
subtitle: Text("Configure server and login settings"),
leading: FaIcon(FontAwesomeIcons.server),
onTap: _editServerSettings,
),
Divider(),
ListTile(
title: Text("About"),
subtitle: Text("App details"),
leading: FaIcon(FontAwesomeIcons.infoCircle),
onTap: _about,
),
],
@ -52,40 +58,8 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
void _about() async {
PackageInfo.fromPlatform().then((PackageInfo info) {
showDialog(
context: context,
child: new SimpleDialog(
title: new Text("About InvenTree"),
children: <Widget>[
ListTile(
title: Text("Server Version"),
subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"),
),
Divider(),
ListTile(
title: Text("App Name"),
subtitle: Text("${info.appName}"),
),
ListTile(
title: Text("Package Name"),
subtitle: Text("${info.packageName}"),
),
ListTile(
title: Text("App Version"),
subtitle: Text("${info.version}"),
),
ListTile(
title: Text("Build Number"),
subtitle: Text("${info.buildNumber}")
),
Divider(),
ListTile(
title: Text("Submit Bug Report"),
subtitle: Text("Submit a bug report or feature request at:\n https://github.com/inventree/inventree-app/issues/"),
)
]
),
);
Navigator.push(context,
MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info)));
});
}
}

View File

@ -169,7 +169,7 @@ class _CategoryDisplayState extends State<CategoryDisplayWidget> {
);
},
body: SubcategoryList(_subcategories),
isExpanded: _subcategoriesExpanded,
isExpanded: _subcategoriesExpanded && _subcategories.length > 0,
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
@ -185,7 +185,7 @@ class _CategoryDisplayState extends State<CategoryDisplayWidget> {
);
},
body: PartList(_parts),
isExpanded: _partListExpanded,
isExpanded: _partListExpanded && _parts.length > 0,
)
],
),

View File

@ -1,10 +1,13 @@
import 'package:InvenTree/api.dart';
import 'package:InvenTree/barcode.dart';
import 'package:flutter/material.dart';
import 'package:InvenTree/api.dart';
import 'package:InvenTree/widget/category_display.dart';
import 'package:InvenTree/widget/location_display.dart';
import 'package:InvenTree/settings.dart';
import 'package:InvenTree/settings/settings.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class InvenTreeDrawer extends StatelessWidget {
@ -33,6 +36,7 @@ class InvenTreeDrawer extends StatelessWidget {
* Upon successful scan, data are passed off to be decoded.
*/
void _scan() async {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
scanQrCode(context);
@ -42,6 +46,7 @@ class InvenTreeDrawer extends StatelessWidget {
* Display the top-level PartCategory list
*/
void _showParts() {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
@ -51,6 +56,7 @@ class InvenTreeDrawer extends StatelessWidget {
* Display the top-level StockLocation list
*/
void _showStock() {
if (!InvenTreeAPI().checkConnection(context)) return;
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
}
@ -72,6 +78,7 @@ class InvenTreeDrawer extends StatelessWidget {
leading: new Image.asset(
"assets/image/icon.png",
fit: BoxFit.scaleDown,
width: 40,
),
title: new Text("InvenTree"),
onTap: _home,

View File

@ -176,7 +176,7 @@ class _LocationDisplayState extends State<LocationDisplayWidget> {
);
},
body: SublocationList(_sublocations),
isExpanded: _locationListExpanded,
isExpanded: _locationListExpanded && _sublocations.length > 0,
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
@ -192,7 +192,7 @@ class _LocationDisplayState extends State<LocationDisplayWidget> {
);
},
body: StockList(_items),
isExpanded: _stockListExpanded,
isExpanded: _stockListExpanded && _items.length > 0,
)
]
),

View File

@ -29,158 +29,126 @@ class _PartDisplayState extends State<PartDisplayWidget> {
InvenTreePart part;
/*
* Construct a list of detail elements about this part.
* Not all elements are set for each part, so only add the ones that are important.
*/
List<Widget> partDetails() {
List<Widget> widgets = [
// Image / name / description
ListTile(
title: Text("${part.fullname}"),
subtitle: Text("${part.description}"),
leading: Image(
image: InvenTreeAPI().getImage(part.image)
),
trailing: IconButton(
icon: FaIcon(FontAwesomeIcons.edit),
onPressed: null,
),
)
];
return widgets;
}
/*
* Build a list of tiles to display under the part description
*/
List<Widget> partTiles() {
List<Widget> tiles = [
List<Widget> tiles = [];
// Image / name / description
tiles.add(
Card(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: partDetails(),
)
),
];
child: ListTile(
title: Text("${part.fullname}"),
subtitle: Text("${part.description}"),
leading: Image(
image: InvenTreeAPI().getImage(part.image)
),
trailing: IconButton(
icon: FaIcon(FontAwesomeIcons.edit),
onPressed: null,
),
)
)
);
// Category information
if (part.categoryName.isNotEmpty) {
tiles.add(
Card(
child: ListTile(
ListTile(
title: Text("Part Category"),
subtitle: Text("${part.categoryName}"),
leading: FaIcon(FontAwesomeIcons.stream),
onTap: () {
InvenTreePartCategory().get(part.categoryId).then((var cat) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat)));
});
if (part.categoryId > 0) {
InvenTreePartCategory().get(part.categoryId).then((var cat) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(cat)));
});
}
},
)
)
);
}
// External link?
if (part.link.isNotEmpty) {
tiles.add(
Card(
child: ListTile(
ListTile(
title: Text("${part.link}"),
leading: FaIcon(FontAwesomeIcons.link),
trailing: Text(""),
onTap: null,
)
)
);
}
// Stock information
tiles.add(
Card(
child: ListTile(
ListTile(
title: Text("Stock"),
leading: FaIcon(FontAwesomeIcons.boxes),
trailing: Text("${part.inStock}"),
onTap: null,
),
)
);
// Parts on order
if (part.isPurchaseable) {
tiles.add(
Card(
child: ListTile(
ListTile(
title: Text("On Order"),
leading: FaIcon(FontAwesomeIcons.shoppingCart),
trailing: Text("${part.onOrder}"),
onTap: null,
)
)
);
}
// Parts being built
if (part.isAssembly) {
tiles.add(
Card(
child: ListTile(
tiles.add(ListTile(
title: Text("Bill of Materials"),
leading: FaIcon(FontAwesomeIcons.thList),
trailing: Text("${part.bomItemCount}"),
onTap: null,
)
)
);
tiles.add(
Card(
child: ListTile(
ListTile(
title: Text("Building"),
leading: FaIcon(FontAwesomeIcons.tools),
trailing: Text("${part.building}"),
onTap: null,
)
)
);
}
if (part.isComponent) {
tiles.add(
Card(
child: ListTile(
tiles.add(ListTile(
title: Text("Used In"),
leading: FaIcon(FontAwesomeIcons.sitemap),
trailing: Text("${part.usedInCount}"),
onTap: null,
)
)
);
}
// Notes field?
if (part.notes.isNotEmpty) {
tiles.add(
Card(
child: ListTile(
ListTile(
title: Text("Notes"),
leading: FaIcon(FontAwesomeIcons.stickyNote),
trailing: Text(""),
onTap: null,
)
)
);
}
tiles.add(Spacer());
return tiles;
}
@ -193,9 +161,7 @@ class _PartDisplayState extends State<PartDisplayWidget> {
),
drawer: new InvenTreeDrawer(context),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
child: ListView(
children: partTiles(),
),
)

View File

@ -1,10 +1,16 @@
import 'package:InvenTree/inventree/stock.dart';
import 'package:InvenTree/inventree/part.dart';
import 'package:InvenTree/widget/location_display.dart';
import 'package:InvenTree/widget/part_display.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:InvenTree/api.dart';
import 'package:InvenTree/widget/drawer.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class StockItemDisplayWidget extends StatefulWidget {
@ -25,27 +31,137 @@ class _StockItemDisplayState extends State<StockItemDisplayWidget> {
final InvenTreeStockItem item;
String get _title {
if (item == null) {
return "Stock Item";
} else {
return "Item: x ${item.partName}";
/*
* Construct a list of detail elements about this StockItem.
* The number of elements may vary depending on the StockItem details
*/
List<Widget> stockTiles() {
List<Widget> tiles = [];
// Image / name / description
tiles.add(
Card(
child: ListTile(
title: Text("${item.partName}"),
subtitle: Text("${item.partDescription}"),
leading: Image(
image: InvenTreeAPI().getImage(item.partImage),
),
trailing: IconButton(
icon: FaIcon(FontAwesomeIcons.edit),
onPressed: null,
)
)
)
);
tiles.add(
ListTile(
title: Text("Part"),
subtitle: Text("${item.partName}"),
leading: FaIcon(FontAwesomeIcons.shapes),
onTap: () {
if (item.partId > 0) {
InvenTreePart().get(item.partId).then((var part) {
if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDisplayWidget(part)));
}
});
}
},
)
);
// Quantity information
tiles.add(
ListTile(
title: Text("Quantity"),
leading: FaIcon(FontAwesomeIcons.cubes),
trailing: Text("${item.quantity}"),
)
);
// Location information
if (item.locationName.isNotEmpty) {
tiles.add(
ListTile(
title: Text("Stock Location"),
subtitle: Text("${item.locationName}"),
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
onTap: () {
if (item.locationId > 0) {
InvenTreeStockLocation().get(item.locationId).then((var loc) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LocationDisplayWidget(loc)));
});
}
},
)
);
}
// Supplier part?
if (item.supplierPartId > 0) {
tiles.add(
ListTile(
title: Text("${item.supplierName}"),
subtitle: Text("${item.supplierSKU}"),
leading: FaIcon(FontAwesomeIcons.industry),
trailing: Image(
image: InvenTreeAPI().getImage(item.supplierImage),
height: 32,
),
onTap: null,
)
);
}
if (item.link.isNotEmpty) {
tiles.add(
ListTile(
title: Text("${item.link}"),
leading: FaIcon(FontAwesomeIcons.link),
trailing: Text(""),
onTap: null,
)
);
}
if (item.trackingItemCount > 0) {
tiles.add(
ListTile(
title: Text("History"),
leading: FaIcon(FontAwesomeIcons.history),
trailing: Text("${item.trackingItemCount}"),
onTap: null,
)
);
}
if (item.notes.isNotEmpty) {
tiles.add(
ListTile(
title: Text("Notes"),
leading: FaIcon(FontAwesomeIcons.stickyNote),
trailing: Text(""),
onTap: null,
)
);
}
return tiles;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
title: Text("Stock Item"),
),
drawer: new InvenTreeDrawer(context),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Stock Item: hello"),
],
child: ListView(
children: stockTiles(),
)
)
);