2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-06-14 19:25:27 +00:00

Merge branch 'create-part'

This commit is contained in:
Oliver
2021-08-15 16:00:36 +10:00
17 changed files with 536 additions and 207 deletions

View File

@ -4,9 +4,13 @@
### 0.4.4 - August 2021 ### 0.4.4 - August 2021
--- ---
- Add ability to create new Part Categories
- Add ability to create new Parts
- Add ability to create new Stock Locations
- Add ability to create new Stock Items
- App bar now always displays "back" button - App bar now always displays "back" button
- Display "batch code" information for stock item - Display "batch code" information for stock item
- Display "packagin" information for stock item - Display "packaging" information for stock item
### 0.4.3 - August 2021 ### 0.4.3 - August 2021
--- ---

View File

@ -540,7 +540,7 @@ class InvenTreeAPI {
* Perform a HTTP POST request * Perform a HTTP POST request
* Returns a json object (or null if unsuccessful) * Returns a json object (or null if unsuccessful)
*/ */
Future<APIResponse> post(String url, {Map<String, dynamic> body = const {}, int expectedStatusCode=201}) async { Future<APIResponse> post(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode=201}) async {
HttpClientRequest? request = await apiRequest(url, "POST"); HttpClientRequest? request = await apiRequest(url, "POST");
@ -763,7 +763,7 @@ class InvenTreeAPI {
* Perform a HTTP GET request * Perform a HTTP GET request
* Returns a json object (or null if did not complete) * Returns a json object (or null if did not complete)
*/ */
Future<APIResponse> get(String url, {Map<String, String> params = const {}, int expectedStatusCode=200}) async { Future<APIResponse> get(String url, {Map<String, String> params = const {}, int? expectedStatusCode=200}) async {
HttpClientRequest? request = await apiRequest( HttpClientRequest? request = await apiRequest(
url, url,

View File

@ -7,6 +7,7 @@ import 'package:inventree/api.dart';
import 'package:inventree/app_colors.dart'; import 'package:inventree/app_colors.dart';
import 'package:inventree/inventree/part.dart'; import 'package:inventree/inventree/part.dart';
import 'package:inventree/inventree/stock.dart'; import 'package:inventree/inventree/stock.dart';
import 'package:inventree/widget/dialogs.dart';
import 'package:inventree/widget/fields.dart'; import 'package:inventree/widget/fields.dart';
import 'package:inventree/l10.dart'; import 'package:inventree/l10.dart';
@ -153,6 +154,9 @@ class APIFormField {
return _constructBoolean(); return _constructBoolean();
case "related field": case "related field":
return _constructRelatedField(); return _constructRelatedField();
case "float":
case "decimal":
return _constructFloatField();
case "choice": case "choice":
return _constructChoiceField(); return _constructChoiceField();
default: default:
@ -202,6 +206,34 @@ class APIFormField {
); );
} }
// Construct a floating point numerical input field
Widget _constructFloatField() {
return TextFormField(
decoration: InputDecoration(
labelText: required ? label + "*" : label,
labelStyle: _labelStyle(),
helperText: helpText,
helperStyle: _helperStyle(),
hintText: placeholderText,
),
initialValue: (value ?? 0).toString(),
keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true),
validator: (value) {
double? quantity = double.tryParse(value.toString()) ?? null;
if (quantity == null) {
return L10().numberInvalid;
}
},
onSaved: (val) {
data["value"] = val;
},
);
}
// Construct an input for a related field // Construct an input for a related field
Widget _constructRelatedField() { Widget _constructRelatedField() {
@ -244,7 +276,16 @@ class APIFormField {
onChanged: null, onChanged: null,
showClearButton: !required, showClearButton: !required,
itemAsString: (dynamic item) { itemAsString: (dynamic item) {
return item['pathstring']; switch (model) {
case "part":
return InvenTreePart.fromJson(item).fullname;
case "partcategory":
return InvenTreePartCategory.fromJson(item).pathstring;
case "stocklocation":
return InvenTreeStockLocation.fromJson(item).pathstring;
default:
return "itemAsString not implemented for '${model}'";
}
}, },
dropdownBuilder: (context, item, itemAsString) { dropdownBuilder: (context, item, itemAsString) {
return _renderRelatedField(item, true, false); return _renderRelatedField(item, true, false);
@ -287,6 +328,22 @@ class APIFormField {
} }
switch (model) { switch (model) {
case "part":
var part = InvenTreePart.fromJson(item);
return ListTile(
title: Text(
part.fullname,
style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal)
),
subtitle: extended ? Text(
part.description,
style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
) : null,
leading: extended ? InvenTreeAPI().getImage(part.thumbnail, width: 40, height: 40) : null,
);
case "partcategory": case "partcategory":
var cat = InvenTreePartCategory.fromJson(item); var cat = InvenTreePartCategory.fromJson(item);
@ -420,7 +477,7 @@ Map<String, dynamic> extractFields(APIResponse response) {
* @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH) * @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH)
*/ */
Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {Map<String, dynamic> modelData = const {}, String method = "PATCH", Function? onSuccess, Function? onCancel}) async { Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {Map<String, dynamic> modelData = const {}, String method = "PATCH", Function(Map<String, dynamic>)? onSuccess, Function? onCancel}) async {
var options = await InvenTreeAPI().options(url); var options = await InvenTreeAPI().options(url);
@ -503,6 +560,7 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
title, title,
url, url,
formFields, formFields,
method,
onSuccess: onSuccess, onSuccess: onSuccess,
)) ))
); );
@ -517,14 +575,18 @@ class APIFormWidget extends StatefulWidget {
//! API URL //! API URL
final String url; final String url;
//! API method
final String method;
final List<APIFormField> fields; final List<APIFormField> fields;
Function? onSuccess; Function(Map<String, dynamic>)? onSuccess;
APIFormWidget( APIFormWidget(
this.title, this.title,
this.url, this.url,
this.fields, this.fields,
this.method,
{ {
Key? key, Key? key,
this.onSuccess, this.onSuccess,
@ -532,7 +594,7 @@ class APIFormWidget extends StatefulWidget {
) : super(key: key); ) : super(key: key);
@override @override
_APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, onSuccess); _APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, method, onSuccess);
} }
@ -545,11 +607,13 @@ class _APIFormWidgetState extends State<APIFormWidget> {
String url; String url;
String method;
List<APIFormField> fields; List<APIFormField> fields;
Function? onSuccess; Function(Map<String, dynamic>)? onSuccess;
_APIFormWidgetState(this.title, this.url, this.fields, this.onSuccess) : super(); _APIFormWidgetState(this.title, this.url, this.fields, this.method, this.onSuccess) : super();
List<Widget> _buildForm() { List<Widget> _buildForm() {
@ -579,35 +643,60 @@ class _APIFormWidgetState extends State<APIFormWidget> {
); );
} }
} }
// Add divider after some widgets
switch (field.type) {
case "related field":
case "choice":
widgets.add(Divider(height: 10));
break;
default:
break;
}
} }
return widgets; return widgets;
} }
Future<APIResponse> _submit(Map<String, String> data) async {
if (method == "POST") {
return await InvenTreeAPI().post(
url,
body: data,
expectedStatusCode: null
);
} else {
return await InvenTreeAPI().patch(
url,
body: data,
expectedStatusCode: null
);
}
}
Future<void> _save(BuildContext context) async { Future<void> _save(BuildContext context) async {
// Package up the form data // Package up the form data
Map<String, String> _data = {}; Map<String, String> data = {};
for (var field in fields) { for (var field in fields) {
dynamic value = field.value; dynamic value = field.value;
if (value == null) { if (value == null) {
_data[field.name] = ""; data[field.name] = "";
} else { } else {
_data[field.name] = value.toString(); data[field.name] = value.toString();
} }
} }
// TODO: Handle "POST" forms too!! final response = await _submit(data);
final response = await InvenTreeAPI().patch(
url,
body: _data,
);
if (!response.isValid()) { if (!response.isValid()) {
// TODO: Display an error message! showServerError(L10().serverError, L10().responseInvalid);
return; return;
} }
@ -625,11 +714,25 @@ class _APIFormWidgetState extends State<APIFormWidget> {
var successFunc = onSuccess; var successFunc = onSuccess;
if (successFunc != null) { if (successFunc != null) {
successFunc();
// Ensure the response is a valid JSON structure
Map<String, dynamic> json = {};
if (response.data != null && response.data is Map) {
for (dynamic key in response.data.keys) {
json[key.toString()] = response.data[key];
}
}
successFunc(json);
} }
return; return;
case 400: case 400:
// Form submission / validation error // Form submission / validation error
showSnackIcon(
L10().error,
success: false
);
// Update field errors // Update field errors
for (var field in fields) { for (var field in fields) {

View File

@ -515,7 +515,7 @@ class _QRViewState extends State<InvenTreeQRView> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(_handler.getOverlayText(context)), title: Text(L10().scanBarcode),
), ),
body: Stack( body: Stack(
children: <Widget>[ children: <Widget>[

View File

@ -13,7 +13,20 @@ class InvenTreeCompany extends InvenTreeModel {
String NAME = "Company"; String NAME = "Company";
@override @override
String URL = "company/"; String get URL => "company/";
@override
Map<String, dynamic> formFields() {
return {
"name": {},
"description": {},
"website": {},
"is_supplier": {},
"is_manufacturer": {},
"is_customer": {},
"currency": {},
};
}
InvenTreeCompany() : super(); InvenTreeCompany() : super();
@ -49,7 +62,7 @@ class InvenTreeCompany extends InvenTreeModel {
*/ */
class InvenTreeSupplierPart extends InvenTreeModel { class InvenTreeSupplierPart extends InvenTreeModel {
@override @override
String URL = "company/part/"; String get URL => "company/part/";
Map<String, String> _filters() { Map<String, String> _filters() {
return { return {

View File

@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../l10.dart'; import '../l10.dart';
import '../api_form.dart';
// Paginated response object // Paginated response object
@ -37,7 +38,7 @@ class InvenTreePageResponse {
class InvenTreeModel { class InvenTreeModel {
// Override the endpoint URL for each subclass // Override the endpoint URL for each subclass
String URL = ""; String get URL => "";
// Override the web URL for each subclass // Override the web URL for each subclass
// Note: If the WEB_URL is the same (except for /api/) as URL then just leave blank // Note: If the WEB_URL is the same (except for /api/) as URL then just leave blank
@ -64,6 +65,49 @@ class InvenTreeModel {
} }
// Fields for editing / creating this model
// Override per-model
Map<String, dynamic> formFields() {
return {};
}
Future<void> createForm(BuildContext context, String title, {Map<String, dynamic> fields=const{}, Map<String, dynamic> data=const {}, Function(dynamic)? onSuccess}) async {
if (fields.isEmpty) {
fields = formFields();
}
launchApiForm(
context,
title,
URL,
fields,
modelData: data,
onSuccess: onSuccess,
method: "POST",
);
}
Future<void> editForm(BuildContext context, String title, {Map<String, dynamic> fields=const {}, Function(dynamic)? onSuccess}) async {
if (fields.isEmpty) {
fields = formFields();
}
launchApiForm(
context,
title,
url,
fields,
modelData: jsondata,
onSuccess: onSuccess,
method: "PATCH"
);
}
// JSON data which defines this object // JSON data which defines this object
Map<String, dynamic> jsondata = {}; Map<String, dynamic> jsondata = {};
@ -155,17 +199,19 @@ class InvenTreeModel {
if (!response.isValid() || response.data == null || !(response.data is Map)) { if (!response.isValid() || response.data == null || !(response.data is Map)) {
// Report error // Report error
await sentryReportMessage( if (response.statusCode > 0) {
"InvenTreeModel.reload() returned invalid response", await sentryReportMessage(
context: { "InvenTreeModel.reload() returned invalid response",
"url": url, context: {
"statusCode": response.statusCode.toString(), "url": url,
"data": response.data?.toString() ?? "null", "statusCode": response.statusCode.toString(),
"valid": response.isValid().toString(), "data": response.data?.toString() ?? "null",
"error": response.error, "valid": response.isValid().toString(),
"errorDetail": response.errorDetail, "error": response.error,
} "errorDetail": response.errorDetail,
); }
);
}
showServerError( showServerError(
L10().serverError, L10().serverError,
@ -226,17 +272,19 @@ class InvenTreeModel {
if (!response.isValid() || response.data == null || !(response.data is Map)) { if (!response.isValid() || response.data == null || !(response.data is Map)) {
await sentryReportMessage( if (response.statusCode > 0) {
"InvenTreeModel.get() returned invalid response", await sentryReportMessage(
context: { "InvenTreeModel.get() returned invalid response",
"url": url, context: {
"statusCode": response.statusCode.toString(), "url": url,
"data": response.data?.toString() ?? "null", "statusCode": response.statusCode.toString(),
"valid": response.isValid().toString(), "data": response.data?.toString() ?? "null",
"error": response.error, "valid": response.isValid().toString(),
"errorDetail": response.errorDetail, "error": response.error,
} "errorDetail": response.errorDetail,
); }
);
}
showServerError( showServerError(
L10().serverError, L10().serverError,
@ -267,17 +315,19 @@ class InvenTreeModel {
// Invalid response returned from server // Invalid response returned from server
if (!response.isValid() || response.data == null || !(response.data is Map)) { if (!response.isValid() || response.data == null || !(response.data is Map)) {
await sentryReportMessage( if (response.statusCode > 0) {
"InvenTreeModel.create() returned invalid response", await sentryReportMessage(
context: { "InvenTreeModel.create() returned invalid response",
"url": url, context: {
"statusCode": response.statusCode.toString(), "url": url,
"data": response.data?.toString() ?? "null", "statusCode": response.statusCode.toString(),
"valid": response.isValid().toString(), "data": response.data?.toString() ?? "null",
"error": response.error, "valid": response.isValid().toString(),
"errorDetail": response.errorDetail, "error": response.error,
} "errorDetail": response.errorDetail,
); }
);
}
showServerError( showServerError(
L10().serverError, L10().serverError,

View File

@ -14,7 +14,17 @@ class InvenTreePartCategory extends InvenTreeModel {
String NAME = "PartCategory"; String NAME = "PartCategory";
@override @override
String URL = "part/category/"; String get URL => "part/category/";
@override
Map<String, dynamic> formFields() {
return {
"name": {},
"description": {},
"parent": {}
};
}
@override @override
Map<String, String> defaultListFilters() { Map<String, String> defaultListFilters() {
@ -68,7 +78,7 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
String NAME = "PartTestTemplate"; String NAME = "PartTestTemplate";
@override @override
String URL = "part/test-template/"; String get URL => "part/test-template/";
String get key => jsondata['key'] ?? ''; String get key => jsondata['key'] ?? '';
@ -125,7 +135,33 @@ class InvenTreePart extends InvenTreeModel {
String NAME = "Part"; String NAME = "Part";
@override @override
String URL = "part/"; String get URL => "part/";
@override
Map<String, dynamic> formFields() {
return {
"name": {},
"description": {},
"IPN": {},
"revision": {},
"keywords": {},
"link": {},
// Parent category
"category": {
},
// Checkbox fields
"active": {},
"assembly": {},
"component": {},
"purchaseable": {},
"salable": {},
"trackable": {},
"is_template": {},
"virtual": {},
};
}
@override @override
Map<String, String> defaultListFilters() { Map<String, String> defaultListFilters() {

View File

@ -18,7 +18,7 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
String NAME = "StockItemTestResult"; String NAME = "StockItemTestResult";
@override @override
String URL = "stock/test/"; String get URL => "stock/test/";
String get key => jsondata['key'] ?? ''; String get key => jsondata['key'] ?? '';
@ -103,11 +103,24 @@ class InvenTreeStockItem extends InvenTreeModel {
String NAME = "StockItem"; String NAME = "StockItem";
@override @override
String URL = "stock/"; String get URL => "stock/";
@override @override
String WEB_URL = "stock/item/"; String WEB_URL = "stock/item/";
@override
Map<String, dynamic> formFields() {
return {
"part": {},
"location": {},
"quantity": {},
"status": {},
"batch": {},
"packaging": {},
"link": {},
};
}
@override @override
Map<String, String> defaultGetFilters() { Map<String, String> defaultGetFilters() {
@ -544,10 +557,19 @@ class InvenTreeStockLocation extends InvenTreeModel {
String NAME = "StockLocation"; String NAME = "StockLocation";
@override @override
String URL = "stock/location/"; String get URL => "stock/location/";
String get pathstring => jsondata['pathstring'] ?? ''; String get pathstring => jsondata['pathstring'] ?? '';
@override
Map<String, dynamic> formFields() {
return {
"name": {},
"description": {},
"parent": {},
};
}
String get parentpathstring { String get parentpathstring {
// TODO - Drive the refactor tractor through this // TODO - Drive the refactor tractor through this
List<String> psplit = pathstring.split('/'); List<String> psplit = pathstring.split('/');

View File

@ -19,8 +19,6 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import '../api_form.dart';
class CategoryDisplayWidget extends StatefulWidget { class CategoryDisplayWidget extends StatefulWidget {
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key); CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
@ -43,27 +41,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
List<Widget> actions = []; List<Widget> actions = [];
/*
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.search),
onPressed: () {
Map<String, String> filters = {};
if (category != null) {
filters["category"] = "${category.pk}";
}
showSearch(
context: context,
delegate: PartSearchDelegate(context, filters: filters)
);
}
)
);
*/
if ((category != null) && InvenTreeAPI().checkPermission('part_category', 'change')) { if ((category != null) && InvenTreeAPI().checkPermission('part_category', 'change')) {
actions.add( actions.add(
IconButton( IconButton(
@ -81,7 +58,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
} }
void _editCategoryDialog(BuildContext context) { void _editCategoryDialog(BuildContext context) {
final _cat = category; final _cat = category;
// Cannot edit top-level category // Cannot edit top-level category
@ -89,17 +65,12 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
return; return;
} }
launchApiForm( _cat.editForm(
context, context,
L10().editCategory, L10().editCategory,
_cat.url, onSuccess: (data) async {
{ refresh();
"name": {}, }
"description": {},
"parent": {},
},
modelData: _cat.jsondata,
onSuccess: refresh,
); );
} }
@ -206,12 +177,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
label: L10().parts, label: L10().parts,
), ),
// TODO - Add the "actions" item back in // TODO - Add the "actions" item back in
/*
BottomNavigationBarItem( BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.wrench), icon: FaIcon(FontAwesomeIcons.wrench),
label: L10().actions label: L10().actions
), ),
*/
] ]
); );
} }
@ -242,18 +211,103 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
return tiles; return tiles;
} }
List<Widget> actionTiles() { Future<void> _newCategory(BuildContext context) async {
int pk = category?.pk ?? -1;
InvenTreePartCategory().createForm(
context,
L10().categoryCreate,
data: {
"parent": (pk > 0) ? pk : null,
},
onSuccess: (data) async {
if (data.containsKey("pk")) {
var cat = InvenTreePartCategory.fromJson(data);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(cat)
)
);
} else {
refresh();
}
}
);
}
Future<void> _newPart() async {
int pk = category?.pk ?? -1;
InvenTreePart().createForm(
context,
L10().partCreate,
data: {
"category": (pk > 0) ? pk : null
},
onSuccess: (data) async {
if (data.containsKey("pk")) {
var part = InvenTreePart.fromJson(data);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartDetailWidget(part)
)
);
}
}
);
}
List<Widget> actionTiles(BuildContext context) {
List<Widget> tiles = [ List<Widget> tiles = [
getCategoryDescriptionCard(extra: false), getCategoryDescriptionCard(extra: false),
ListTile(
title: Text(L10().actions,
style: TextStyle(fontWeight: FontWeight.bold)
)
)
]; ];
// TODO - Actions! if (InvenTreeAPI().checkPermission('part', 'add')) {
tiles.add(
ListTile(
title: Text(L10().categoryCreate),
subtitle: Text(L10().categoryCreateDetail),
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
onTap: () async {
_newCategory(context);
},
)
);
if (category != null) {
tiles.add(
ListTile(
title: Text(L10().partCreate),
subtitle: Text(L10().partCreateDetail),
leading: FaIcon(FontAwesomeIcons.shapes, color: COLOR_CLICK),
onTap: _newPart,
)
);
}
}
if (tiles.length == 0) {
tiles.add(
ListTile(
title: Text(
L10().actionsNone
),
subtitle: Text(
L10().permissionAccountDenied,
),
leading: FaIcon(FontAwesomeIcons.userTimes),
)
);
}
return tiles; return tiles;
} }
@ -274,7 +328,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
); );
case 2: case 2:
return ListView( return ListView(
children: actionTiles() children: actionTiles(context)
); );
default: default:
return ListView(); return ListView();

View File

@ -65,21 +65,12 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
void editCompany(BuildContext context) async { void editCompany(BuildContext context) async {
launchApiForm( company.editForm(
context, context,
L10().companyEdit, L10().companyEdit,
company.url, onSuccess: (data) async {
{ refresh();
"name": {}, }
"description": {},
"website": {},
"is_supplier": {},
"is_manufacturer": {},
"is_customer": {},
"currency": {},
},
modelData: company.jsondata,
onSuccess: refresh
); );
} }

View File

@ -1,5 +1,4 @@
import 'package:inventree/api.dart'; import 'package:inventree/api.dart';
import 'package:inventree/api_form.dart';
import 'package:inventree/app_colors.dart'; import 'package:inventree/app_colors.dart';
import 'package:inventree/app_settings.dart'; import 'package:inventree/app_settings.dart';
import 'package:inventree/barcode.dart'; import 'package:inventree/barcode.dart';
@ -84,17 +83,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
return; return;
} }
launchApiForm( _loc.editForm(
context, context,
L10().editLocation, L10().editLocation,
_loc.url, onSuccess: (data) async {
{ refresh();
"name": {}, }
"description": {},
"parent": {},
},
modelData: _loc.jsondata,
onSuccess: refresh
); );
} }
@ -142,6 +136,61 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
setState(() {}); setState(() {});
} }
Future<void> _newLocation(BuildContext context) async {
int pk = location?.pk ?? -1;
InvenTreeStockLocation().createForm(
context,
L10().locationCreate,
data: {
"parent": (pk > 0) ? pk : null,
},
onSuccess: (data) async {
if (data.containsKey("pk")) {
var loc = InvenTreeStockLocation.fromJson(data);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LocationDisplayWidget(loc)
)
);
}
}
);
}
Future<void> _newStockItem(BuildContext context) async {
int pk = location?.pk ?? -1;
if (pk <= 0) {
return;
}
InvenTreeStockItem().createForm(
context,
L10().stockItemCreate,
data: {
"location": pk,
},
onSuccess: (data) async {
if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockDetailWidget(item)
)
);
}
}
);
}
Widget locationDescriptionCard({bool includeActions = true}) { Widget locationDescriptionCard({bool includeActions = true}) {
if (location == null) { if (location == null) {
return Card( return Card(
@ -206,7 +255,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
icon: FaIcon(FontAwesomeIcons.boxes), icon: FaIcon(FontAwesomeIcons.boxes),
label: L10().stock, label: L10().stock,
), ),
// TODO - Add in actions when they are written...
BottomNavigationBarItem( BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.wrench), icon: FaIcon(FontAwesomeIcons.wrench),
label: L10().actions, label: L10().actions,
@ -283,6 +331,32 @@ List<Widget> detailTiles() {
tiles.add(locationDescriptionCard(includeActions: false)); tiles.add(locationDescriptionCard(includeActions: false));
if (InvenTreeAPI().checkPermission('stock', 'add')) {
tiles.add(
ListTile(
title: Text(L10().locationCreate),
subtitle: Text(L10().locationCreateDetail),
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
onTap: () async {
_newLocation(context);
},
)
);
tiles.add(
ListTile(
title: Text(L10().stockItemCreate),
subtitle: Text(L10().stockItemCreateDetail),
leading: FaIcon(FontAwesomeIcons.boxes, color: COLOR_CLICK),
onTap: () async {
_newStockItem(context);
},
)
);
}
if (location != null) { if (location != null) {
// Stock adjustment actions // Stock adjustment actions
if (InvenTreeAPI().checkPermission('stock', 'change')) { if (InvenTreeAPI().checkPermission('stock', 'change')) {

View File

@ -6,12 +6,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:inventree/app_colors.dart'; import 'package:inventree/app_colors.dart';
import 'package:inventree/l10.dart'; import 'package:inventree/l10.dart';
import 'package:inventree/api_form.dart';
import 'package:inventree/widget/part_notes.dart'; import 'package:inventree/widget/part_notes.dart';
import 'package:inventree/widget/progress.dart'; import 'package:inventree/widget/progress.dart';
import 'package:inventree/inventree/part.dart'; import 'package:inventree/inventree/part.dart';
import 'package:inventree/widget/category_display.dart'; import 'package:inventree/widget/category_display.dart';
import 'package:inventree/widget/part_suppliers.dart';
import 'package:inventree/api.dart'; import 'package:inventree/api.dart';
import 'package:inventree/widget/refreshable_state.dart'; import 'package:inventree/widget/refreshable_state.dart';
import 'package:inventree/widget/part_image_widget.dart'; import 'package:inventree/widget/part_image_widget.dart';
@ -100,33 +98,12 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
void _editPartDialog(BuildContext context) { void _editPartDialog(BuildContext context) {
launchApiForm( part.editForm(
context, context,
L10().editPart, L10().editPart,
part.url, onSuccess: (data) async {
{ refresh();
"name": {}, }
"description": {},
"IPN": {},
"revision": {},
"keywords": {},
"link": {},
"category": {
},
// Checkbox fields
"active": {},
"assembly": {},
"component": {},
"purchaseable": {},
"salable": {},
"trackable": {},
"is_template": {},
"virtual": {},
},
modelData: part.jsondata,
onSuccess: refresh,
); );
} }
@ -305,16 +282,18 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
// Tiles for an "assembly" part // Tiles for an "assembly" part
if (part.isAssembly) { if (part.isAssembly) {
tiles.add( if (part.bomItemCount > 0) {
ListTile( tiles.add(
title: Text(L10().billOfMaterials), ListTile(
leading: FaIcon(FontAwesomeIcons.thList), title: Text(L10().billOfMaterials),
trailing: Text("${part.bomItemCount}"), leading: FaIcon(FontAwesomeIcons.thList),
onTap: () { trailing: Text("${part.bomItemCount}"),
// TODO onTap: () {
} // TODO
) }
); )
);
}
if (part.building > 0) { if (part.building > 0) {
tiles.add( tiles.add(
@ -331,7 +310,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
} }
// Tiles for "component" part // Tiles for "component" part
if (part.isComponent) { if (part.isComponent && part.usedInCount > 0) {
tiles.add( tiles.add(
ListTile( ListTile(
@ -422,6 +401,19 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
), ),
); );
if (false && !part.isActive && InvenTreeAPI().checkPermission('part', 'delete')) {
tiles.add(
ListTile(
title: Text(L10().deletePart),
subtitle: Text(L10().deletePartDetail),
leading: FaIcon(FontAwesomeIcons.trashAlt, color: COLOR_DANGER),
onTap: () {
// TODO
},
)
);
}
return tiles; return tiles;
} }
@ -469,12 +461,10 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
label: L10().stock label: L10().stock
), ),
// TODO - Add part actions // TODO - Add part actions
/*
BottomNavigationBarItem( BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.wrench), icon: FaIcon(FontAwesomeIcons.wrench),
label: L10().actions, label: L10().actions,
), ),
*/
] ]
); );
} }

View File

@ -7,8 +7,6 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:inventree/l10.dart'; import 'package:inventree/l10.dart';
import '../api_form.dart';
class PartNotesWidget extends StatefulWidget { class PartNotesWidget extends StatefulWidget {
@ -46,17 +44,15 @@ class _PartNotesState extends RefreshableState<PartNotesWidget> {
icon: FaIcon(FontAwesomeIcons.edit), icon: FaIcon(FontAwesomeIcons.edit),
tooltip: L10().edit, tooltip: L10().edit,
onPressed: () { onPressed: () {
launchApiForm( part.editForm(
context, context,
L10().editNotes, L10().editNotes,
part.url, fields: {
{
"notes": { "notes": {
"multiline": true, "multiline": true,
} }
}, },
modelData: part.jsondata, onSuccess: (data) async {
onSuccess: () async {
refresh(); refresh();
} }
); );

View File

@ -22,8 +22,6 @@ import 'package:inventree/api.dart';
import 'package:dropdown_search/dropdown_search.dart'; import 'package:dropdown_search/dropdown_search.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../api_form.dart';
class StockDetailWidget extends StatefulWidget { class StockDetailWidget extends StatefulWidget {
StockDetailWidget(this.item, {Key? key}) : super(key: key); StockDetailWidget(this.item, {Key? key}) : super(key: key);
@ -109,18 +107,20 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
void _editStockItem(BuildContext context) async { void _editStockItem(BuildContext context) async {
launchApiForm( var fields = InvenTreeStockItem().formFields();
// Some fields we don't want to edit!
fields.remove("part");
fields.remove("quantity");
fields.remove("location");
item.editForm(
context, context,
L10().editItem, L10().editItem,
item.url, fields: fields,
{ onSuccess: (data) async {
"status": {}, refresh();
"batch": {}, }
"packaging": {},
"link": {},
},
modelData: item.jsondata,
onSuccess: refresh
); );
} }

View File

@ -8,7 +8,6 @@ import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:inventree/l10.dart'; import 'package:inventree/l10.dart';
import '../api.dart'; import '../api.dart';
import '../api_form.dart';
class StockNotesWidget extends StatefulWidget { class StockNotesWidget extends StatefulWidget {
@ -46,19 +45,17 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> {
icon: FaIcon(FontAwesomeIcons.edit), icon: FaIcon(FontAwesomeIcons.edit),
tooltip: L10().edit, tooltip: L10().edit,
onPressed: () { onPressed: () {
launchApiForm( item.editForm(
context, context,
L10().editNotes, L10().editNotes,
item.url, fields: {
{ "notes": {
"notes": { "multiline": true,
"multiline": true,
}
},
modelData: item.jsondata,
onSuccess: () {
refresh();
} }
},
onSuccess: (data) async {
refresh();
}
); );
} }
) )

View File

@ -1,6 +1,5 @@
import 'package:email_validator/email_validator.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';