mirror of
https://github.com/inventree/inventree-app.git
synced 2025-05-05 08:48:55 +00:00
Merge branch 'stock-actions'
This commit is contained in:
commit
14617686b2
21
lib/api.dart
21
lib/api.dart
@ -59,7 +59,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
String makeApiUrl(String endpoint) {
|
String makeApiUrl(String endpoint) {
|
||||||
|
|
||||||
return apiUrl + endpoint;
|
return _makeUrl("/api/" + endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
String makeUrl(String endpoint) {
|
String makeUrl(String endpoint) {
|
||||||
@ -272,20 +272,18 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform a POST request
|
// Perform a POST request
|
||||||
Future<http.Response> post(String url, {Map<String, String> body}) async {
|
Future<http.Response> post(String url, {Map<String, dynamic> body}) async {
|
||||||
|
|
||||||
var _url = makeApiUrl(url);
|
var _url = makeApiUrl(url);
|
||||||
var _headers = defaultHeaders();
|
var _headers = jsonHeaders();
|
||||||
var _body = Map<String, String>();
|
|
||||||
|
|
||||||
// Copy across provided data
|
print("POST: ${_url} -> ${body.toString()}");
|
||||||
body.forEach((K, V) => _body[K] = V);
|
|
||||||
|
|
||||||
print("POST: " + _url);
|
var data = jsonEncode(body);
|
||||||
|
|
||||||
return http.post(_url,
|
return http.post(_url,
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
body: _body,
|
body: data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,6 +322,13 @@ class InvenTreeAPI {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> jsonHeaders() {
|
||||||
|
|
||||||
|
var headers = defaultHeaders();
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
String _authorizationHeader () {
|
String _authorizationHeader () {
|
||||||
if (_token.isNotEmpty) {
|
if (_token.isNotEmpty) {
|
||||||
return "Token $_token";
|
return "Token $_token";
|
||||||
|
@ -56,7 +56,8 @@ class InvenTreeModel {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get url{ return path.join(URL, pk.toString()); }
|
// Return the API detail endpoint for this Model object
|
||||||
|
String get url => "${URL}/${pk}/";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Search this Model type in the database
|
// Search this Model type in the database
|
||||||
@ -79,6 +80,25 @@ class InvenTreeModel {
|
|||||||
// A map of "default" headers to use when performing a GET request
|
// A map of "default" headers to use when performing a GET request
|
||||||
Map<String, String> defaultGetFilters() { return Map<String, String>(); }
|
Map<String, String> defaultGetFilters() { return Map<String, String>(); }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reload this object, by requesting data from the server
|
||||||
|
*/
|
||||||
|
Future<bool> reload() async {
|
||||||
|
|
||||||
|
var response = await api.get(url, params: defaultGetFilters());
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
print("Error retrieving data");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, dynamic> data = json.decode(response.body);
|
||||||
|
|
||||||
|
jsondata = data;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Return the detail view for the associated pk
|
// Return the detail view for the associated pk
|
||||||
Future<InvenTreeModel> get(int pk, {Map<String, String> filters}) async {
|
Future<InvenTreeModel> get(int pk, {Map<String, String> filters}) async {
|
||||||
|
|
||||||
@ -102,7 +122,7 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
print("GET: $addr ${params.toString()}");
|
print("GET: $addr ${params.toString()}");
|
||||||
|
|
||||||
var response = await InvenTreeAPI().get(addr, params: params);
|
var response = await api.get(addr, params: params);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
print("Error retrieving data");
|
print("Error retrieving data");
|
||||||
@ -134,7 +154,7 @@ class InvenTreeModel {
|
|||||||
// TODO - Add "timeout"
|
// TODO - Add "timeout"
|
||||||
// TODO - Add error catching
|
// TODO - Add error catching
|
||||||
|
|
||||||
var response = await InvenTreeAPI().get(URL, params:params);
|
var response = await api.get(URL, params:params);
|
||||||
|
|
||||||
// A list of "InvenTreeModel" items
|
// A list of "InvenTreeModel" items
|
||||||
List<InvenTreeModel> results = new List<InvenTreeModel>();
|
List<InvenTreeModel> results = new List<InvenTreeModel>();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'model.dart';
|
import 'model.dart';
|
||||||
|
|
||||||
import 'package:InvenTree/api.dart';
|
import 'package:InvenTree/api.dart';
|
||||||
@ -121,6 +122,8 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
int get locationId => jsondata['location'] as int ?? -1;
|
int get locationId => jsondata['location'] as int ?? -1;
|
||||||
|
|
||||||
|
bool isSerialized() => serialNumber != null && quantity.toInt() == 1;
|
||||||
|
|
||||||
String get locationName {
|
String get locationName {
|
||||||
String loc = '';
|
String loc = '';
|
||||||
|
|
||||||
@ -153,6 +156,54 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<http.Response> countStock(double quan, {String notes}) async {
|
||||||
|
|
||||||
|
// Cannot 'count' a serialized StockItem
|
||||||
|
if (isSerialized()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot count negative stock
|
||||||
|
if (quan < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.post("/stock/count/", body: {
|
||||||
|
"item": {
|
||||||
|
"pk": "${pk}",
|
||||||
|
"quantity": "${quan}",
|
||||||
|
},
|
||||||
|
"notes": notes ?? '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> addStock(double quan, {String notes}) async {
|
||||||
|
|
||||||
|
if (isSerialized() || quan <= 0) return null;
|
||||||
|
|
||||||
|
return api.post("/stock/add/", body: {
|
||||||
|
"item": {
|
||||||
|
"pk": "${pk}",
|
||||||
|
"quantity": "${quan}",
|
||||||
|
},
|
||||||
|
"notes": notes ?? '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> removeStock(double quan, {String notes}) async {
|
||||||
|
|
||||||
|
if (isSerialized() || quan <= 0) return null;
|
||||||
|
|
||||||
|
return api.post("/stock/remove/", body: {
|
||||||
|
"item": {
|
||||||
|
"pk": "${pk}",
|
||||||
|
"quantity": "${quan}",
|
||||||
|
},
|
||||||
|
"notes": notes ?? '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -194,5 +245,4 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
|||||||
|
|
||||||
return loc;
|
return loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ import 'package:InvenTree/inventree/stock.dart';
|
|||||||
import 'package:InvenTree/widget/category_display.dart';
|
import 'package:InvenTree/widget/category_display.dart';
|
||||||
import 'package:InvenTree/widget/company_list.dart';
|
import 'package:InvenTree/widget/company_list.dart';
|
||||||
import 'package:InvenTree/widget/location_display.dart';
|
import 'package:InvenTree/widget/location_display.dart';
|
||||||
|
import 'package:InvenTree/widget/search.dart';
|
||||||
import 'package:InvenTree/widget/drawer.dart';
|
import 'package:InvenTree/widget/drawer.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -192,7 +193,8 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
void _search() {
|
void _search() {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
// TODO
|
Navigator.push(context, MaterialPageRoute(builder: (context) => SearchWidget()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scan() {
|
void _scan() {
|
||||||
@ -251,7 +253,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.search),
|
icon: FaIcon(FontAwesomeIcons.search),
|
||||||
tooltip: 'Search',
|
tooltip: 'Search',
|
||||||
onPressed: null,
|
onPressed: _search,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -271,7 +273,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: new FaIcon(FontAwesomeIcons.search),
|
icon: new FaIcon(FontAwesomeIcons.search),
|
||||||
tooltip: 'Search',
|
tooltip: 'Search',
|
||||||
onPressed: _unsupported,
|
onPressed: _search,
|
||||||
),
|
),
|
||||||
Text("Search"),
|
Text("Search"),
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:InvenTree/api.dart';
|
import 'package:InvenTree/api.dart';
|
||||||
import 'package:InvenTree/barcode.dart';
|
import 'package:InvenTree/barcode.dart';
|
||||||
import 'package:InvenTree/widget/company_list.dart';
|
import 'package:InvenTree/widget/company_list.dart';
|
||||||
|
import 'package:InvenTree/widget/search.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:InvenTree/api.dart';
|
import 'package:InvenTree/api.dart';
|
||||||
@ -32,6 +33,11 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false);
|
Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _search() {
|
||||||
|
_closeDrawer();
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => SearchWidget()));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Launch the camera to scan a QR code.
|
* Launch the camera to scan a QR code.
|
||||||
* Upon successful scan, data are passed off to be decoded.
|
* Upon successful scan, data are passed off to be decoded.
|
||||||
@ -102,7 +108,7 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
new ListTile(
|
new ListTile(
|
||||||
title: new Text("Search"),
|
title: new Text("Search"),
|
||||||
leading: new FaIcon(FontAwesomeIcons.search),
|
leading: new FaIcon(FontAwesomeIcons.search),
|
||||||
onTap: null,
|
onTap: _search,
|
||||||
),
|
),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
title: new Text("Scan Barcode"),
|
title: new Text("Scan Barcode"),
|
||||||
|
@ -160,6 +160,11 @@ class _PartDisplayState extends State<PartDetailWidget> {
|
|||||||
title: Text("Part Details"),
|
title: Text("Part Details"),
|
||||||
),
|
),
|
||||||
drawer: new InvenTreeDrawer(context),
|
drawer: new InvenTreeDrawer(context),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
child: FaIcon(FontAwesomeIcons.ellipsisH),
|
||||||
|
// TODO - Add pop-up icons
|
||||||
|
// Ref: https://stackoverflow.com/questions/46480221/flutter-floating-action-button-with-speed-dial#46480722
|
||||||
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: partTiles(),
|
children: partTiles(),
|
||||||
|
33
lib/widget/search.dart
Normal file
33
lib/widget/search.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
import 'package:InvenTree/widget/drawer.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SearchWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SearchState createState() => _SearchState();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _SearchState extends State<SearchWidget> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("Search"),
|
||||||
|
),
|
||||||
|
drawer: new InvenTreeDrawer(context),
|
||||||
|
body: Center(
|
||||||
|
child: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:InvenTree/api.dart';
|
import 'package:InvenTree/api.dart';
|
||||||
|
|
||||||
import 'package:InvenTree/widget/drawer.dart';
|
import 'package:InvenTree/widget/drawer.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';
|
||||||
|
|
||||||
class StockDetailWidget extends StatefulWidget {
|
class StockDetailWidget extends StatefulWidget {
|
||||||
|
|
||||||
@ -25,12 +27,270 @@ class StockDetailWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _StockItemDisplayState extends State<StockDetailWidget> {
|
class _StockItemDisplayState extends State<StockDetailWidget> {
|
||||||
|
|
||||||
|
final TextEditingController _quantityController = TextEditingController();
|
||||||
|
final TextEditingController _notesController = TextEditingController();
|
||||||
|
|
||||||
|
final _addStockKey = GlobalKey<FormState>();
|
||||||
|
final _removeStockKey = GlobalKey<FormState>();
|
||||||
|
final _countStockKey = GlobalKey<FormState>();
|
||||||
|
final _moveStockKey = GlobalKey<FormState>();
|
||||||
|
final _editStockKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
_StockItemDisplayState(this.item) {
|
_StockItemDisplayState(this.item) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
final InvenTreeStockItem item;
|
final InvenTreeStockItem item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to reload the page data
|
||||||
|
*/
|
||||||
|
Future<void> _refresh() async {
|
||||||
|
|
||||||
|
await item.reload();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _editStockItem() {
|
||||||
|
// TODO - Form for editing stock item
|
||||||
|
}
|
||||||
|
|
||||||
|
void _editStockItemDialog() {
|
||||||
|
|
||||||
|
return;
|
||||||
|
// TODO - Finish implementing this
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text("Edit Stock Item"),
|
||||||
|
actions: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text("Save"),
|
||||||
|
onPressed: () {
|
||||||
|
if (_editStockKey.currentState.validate()) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addStock() async {
|
||||||
|
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
double quantity = double.parse(_quantityController.text);
|
||||||
|
_quantityController.clear();
|
||||||
|
|
||||||
|
// Await response to prevent the button from being pressed multiple times
|
||||||
|
var response = await item.addStock(quantity, notes: _notesController.text);
|
||||||
|
_notesController.clear();
|
||||||
|
|
||||||
|
// TODO - Handle error cases
|
||||||
|
_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addStockDialog() async {
|
||||||
|
showDialog(context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text("Add Stock"),
|
||||||
|
actions: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text("Add"),
|
||||||
|
onPressed: () {
|
||||||
|
if (_addStockKey.currentState.validate()) _addStock();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
content: Form(
|
||||||
|
key: _addStockKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Current Quantity: ${item.quantity}"),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Add stock",
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||||
|
controller: _quantityController,
|
||||||
|
validator: (value) {
|
||||||
|
if (value.isEmpty) return "Value cannot be empty";
|
||||||
|
|
||||||
|
double quantity = double.tryParse(value);
|
||||||
|
if (quantity == null) return "Value cannot be converted to a number";
|
||||||
|
if (quantity <= 0) return "Value must be positive";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Notes",
|
||||||
|
),
|
||||||
|
controller: _notesController,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// TODO - Form for adding stock
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeStock() async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
double quantity = double.parse(_quantityController.text);
|
||||||
|
_quantityController.clear();
|
||||||
|
|
||||||
|
var response = await item.removeStock(quantity, notes: _notesController.text);
|
||||||
|
_notesController.clear();
|
||||||
|
|
||||||
|
// TODO - Handle error cases
|
||||||
|
|
||||||
|
_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeStockDialog() {
|
||||||
|
showDialog(context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text("Remove Stock"),
|
||||||
|
actions: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text("Remove"),
|
||||||
|
onPressed: () {
|
||||||
|
if (_removeStockKey.currentState.validate()) _removeStock();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
content: Form(
|
||||||
|
key: _removeStockKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Current quantity: ${item.quantity}"),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Remove stock",
|
||||||
|
),
|
||||||
|
controller: _quantityController,
|
||||||
|
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||||
|
validator: (value) {
|
||||||
|
if (value.isEmpty) return "Value cannot be empty";
|
||||||
|
|
||||||
|
double quantity = double.tryParse(value);
|
||||||
|
|
||||||
|
if (quantity == null) return "Value cannot be converted to a number";
|
||||||
|
if (quantity <= 0) return "Value must be positive";
|
||||||
|
|
||||||
|
if (quantity > item.quantity) return "Cannot take more than current quantity";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Notes",
|
||||||
|
),
|
||||||
|
controller: _notesController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _countStock() async {
|
||||||
|
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
double quantity = double.parse(_quantityController.text);
|
||||||
|
_quantityController.clear();
|
||||||
|
|
||||||
|
var response = await item.countStock(quantity, notes: _notesController.text);
|
||||||
|
_notesController.clear();
|
||||||
|
|
||||||
|
// TODO - Handle error cases
|
||||||
|
|
||||||
|
_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _countStockDialog() async {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text("Count Stock"),
|
||||||
|
actions: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text("Count"),
|
||||||
|
onPressed: () {
|
||||||
|
if (_countStockKey.currentState.validate()) _countStock();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
content: Form(
|
||||||
|
key: _countStockKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Count stock",
|
||||||
|
hintText: "${item.quantity}",
|
||||||
|
),
|
||||||
|
controller: _quantityController,
|
||||||
|
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||||
|
validator: (value) {
|
||||||
|
if (value.isEmpty) return "Value cannot be empty";
|
||||||
|
|
||||||
|
double quantity = double.tryParse(value);
|
||||||
|
if (quantity == null) return "Value cannot be converted to a number";
|
||||||
|
if (quantity < 0) return "Value cannot be negative";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Notes",
|
||||||
|
),
|
||||||
|
controller: _notesController,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void _transferStock(int location) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
void _transferStockDialog() {
|
||||||
|
// TODO - Form for transferring stock
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct a list of detail elements about this StockItem.
|
* Construct a list of detail elements about this StockItem.
|
||||||
* The number of elements may vary depending on the StockItem details
|
* The number of elements may vary depending on the StockItem details
|
||||||
@ -49,7 +309,7 @@ class _StockItemDisplayState extends State<StockDetailWidget> {
|
|||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
onPressed: null,
|
onPressed: _editStockItemDialog,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -73,13 +333,25 @@ class _StockItemDisplayState extends State<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Quantity information
|
// Quantity information
|
||||||
tiles.add(
|
if (item.isSerialized()) {
|
||||||
ListTile(
|
tiles.add(
|
||||||
title: Text("Quantity"),
|
ListTile(
|
||||||
leading: FaIcon(FontAwesomeIcons.cubes),
|
title: Text("Serial Number"),
|
||||||
trailing: Text("${item.quantity}"),
|
leading: FaIcon(FontAwesomeIcons.hashtag),
|
||||||
)
|
trailing: Text("${item.serialNumber}"),
|
||||||
);
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text("Quantity"),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.cubes),
|
||||||
|
trailing: Text("${item.quantity}"),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Location information
|
// Location information
|
||||||
if (item.locationName.isNotEmpty) {
|
if (item.locationName.isNotEmpty) {
|
||||||
@ -152,6 +424,46 @@ class _StockItemDisplayState extends State<StockDetailWidget> {
|
|||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return a list of context-sensitive action buttons.
|
||||||
|
* Not all buttons will be avaialable for a given StockItem,
|
||||||
|
* depending on the properties of that StockItem
|
||||||
|
*/
|
||||||
|
List<SpeedDialChild> actionButtons() {
|
||||||
|
var buttons = List<SpeedDialChild>();
|
||||||
|
|
||||||
|
// The following actions only apply if the StockItem is not serialized
|
||||||
|
if (!item.isSerialized()) {
|
||||||
|
buttons.add(SpeedDialChild(
|
||||||
|
child: Icon(FontAwesomeIcons.plusCircle),
|
||||||
|
label: "Add Stock",
|
||||||
|
onTap: _addStockDialog,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.add(SpeedDialChild(
|
||||||
|
child: Icon(FontAwesomeIcons.minusCircle),
|
||||||
|
label: "Remove Stock",
|
||||||
|
onTap: _removeStockDialog,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.add(SpeedDialChild(
|
||||||
|
child: Icon(FontAwesomeIcons.checkCircle),
|
||||||
|
label: "Count Stock",
|
||||||
|
onTap: _countStockDialog,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.add(SpeedDialChild(
|
||||||
|
child: Icon(FontAwesomeIcons.exchangeAlt),
|
||||||
|
label: "Transfer Stock",
|
||||||
|
onTap: _transferStockDialog,
|
||||||
|
));
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -159,9 +471,18 @@ class _StockItemDisplayState extends State<StockDetailWidget> {
|
|||||||
title: Text("Stock Item"),
|
title: Text("Stock Item"),
|
||||||
),
|
),
|
||||||
drawer: new InvenTreeDrawer(context),
|
drawer: new InvenTreeDrawer(context),
|
||||||
|
floatingActionButton: SpeedDial(
|
||||||
|
visible: true,
|
||||||
|
animatedIcon: AnimatedIcons.menu_close,
|
||||||
|
heroTag: 'stock-item-fab',
|
||||||
|
children: actionButtons(),
|
||||||
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: ListView(
|
child: new RefreshIndicator(
|
||||||
children: stockTiles(),
|
onRefresh: _refresh,
|
||||||
|
child: ListView(
|
||||||
|
children: stockTiles(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -83,6 +83,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.4"
|
||||||
|
flutter_speed_dial:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_speed_dial
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.5"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -225,7 +232,7 @@ packages:
|
|||||||
name: preferences
|
name: preferences
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.2.0"
|
||||||
qr_utils:
|
qr_utils:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
15
pubspec.yaml
15
pubspec.yaml
@ -26,15 +26,12 @@ dependencies:
|
|||||||
http: ^0.12.0+2
|
http: ^0.12.0+2
|
||||||
shared_preferences: ^0.5.3+1
|
shared_preferences: ^0.5.3+1
|
||||||
|
|
||||||
flutter_advanced_networkimage: any
|
flutter_advanced_networkimage: any # Pull image from network or cache
|
||||||
|
preferences: ^5.1.0 # Persistent settings storage
|
||||||
preferences: ^5.1.0
|
qr_utils: ^0.1.4 # Barcode / QR-code support
|
||||||
|
package_info: ^0.4.0+16 # App information introspection
|
||||||
qr_utils: ^0.1.4
|
font_awesome_flutter: ^8.8.1 # FontAwesome icon set
|
||||||
|
flutter_speed_dial: ^1.2.5 # FAB menu elements
|
||||||
package_info: ^0.4.0+16
|
|
||||||
|
|
||||||
font_awesome_flutter: ^8.8.1
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user