mirror of
https://github.com/inventree/inventree-app.git
synced 2025-06-13 10:45:29 +00:00
Merge branch 'create-part'
This commit is contained in:
@ -4,9 +4,13 @@
|
||||
### 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
|
||||
- Display "batch code" information for stock item
|
||||
- Display "packagin" information for stock item
|
||||
- Display "packaging" information for stock item
|
||||
|
||||
### 0.4.3 - August 2021
|
||||
---
|
||||
|
@ -540,7 +540,7 @@ class InvenTreeAPI {
|
||||
* Perform a HTTP POST request
|
||||
* 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");
|
||||
|
||||
@ -763,7 +763,7 @@ class InvenTreeAPI {
|
||||
* Perform a HTTP GET request
|
||||
* 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(
|
||||
url,
|
||||
|
@ -7,6 +7,7 @@ import 'package:inventree/api.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
@ -153,6 +154,9 @@ class APIFormField {
|
||||
return _constructBoolean();
|
||||
case "related field":
|
||||
return _constructRelatedField();
|
||||
case "float":
|
||||
case "decimal":
|
||||
return _constructFloatField();
|
||||
case "choice":
|
||||
return _constructChoiceField();
|
||||
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
|
||||
Widget _constructRelatedField() {
|
||||
|
||||
@ -244,7 +276,16 @@ class APIFormField {
|
||||
onChanged: null,
|
||||
showClearButton: !required,
|
||||
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) {
|
||||
return _renderRelatedField(item, true, false);
|
||||
@ -287,6 +328,22 @@ class APIFormField {
|
||||
}
|
||||
|
||||
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":
|
||||
|
||||
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)
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
@ -503,6 +560,7 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
|
||||
title,
|
||||
url,
|
||||
formFields,
|
||||
method,
|
||||
onSuccess: onSuccess,
|
||||
))
|
||||
);
|
||||
@ -517,14 +575,18 @@ class APIFormWidget extends StatefulWidget {
|
||||
//! API URL
|
||||
final String url;
|
||||
|
||||
//! API method
|
||||
final String method;
|
||||
|
||||
final List<APIFormField> fields;
|
||||
|
||||
Function? onSuccess;
|
||||
Function(Map<String, dynamic>)? onSuccess;
|
||||
|
||||
APIFormWidget(
|
||||
this.title,
|
||||
this.url,
|
||||
this.fields,
|
||||
this.method,
|
||||
{
|
||||
Key? key,
|
||||
this.onSuccess,
|
||||
@ -532,7 +594,7 @@ class APIFormWidget extends StatefulWidget {
|
||||
) : super(key: key);
|
||||
|
||||
@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 method;
|
||||
|
||||
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() {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
// Package up the form data
|
||||
Map<String, String> _data = {};
|
||||
Map<String, String> data = {};
|
||||
|
||||
for (var field in fields) {
|
||||
|
||||
dynamic value = field.value;
|
||||
|
||||
if (value == null) {
|
||||
_data[field.name] = "";
|
||||
data[field.name] = "";
|
||||
} else {
|
||||
_data[field.name] = value.toString();
|
||||
data[field.name] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle "POST" forms too!!
|
||||
final response = await InvenTreeAPI().patch(
|
||||
url,
|
||||
body: _data,
|
||||
);
|
||||
final response = await _submit(data);
|
||||
|
||||
if (!response.isValid()) {
|
||||
// TODO: Display an error message!
|
||||
showServerError(L10().serverError, L10().responseInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -625,11 +714,25 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
var successFunc = onSuccess;
|
||||
|
||||
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;
|
||||
case 400:
|
||||
// Form submission / validation error
|
||||
showSnackIcon(
|
||||
L10().error,
|
||||
success: false
|
||||
);
|
||||
|
||||
// Update field errors
|
||||
for (var field in fields) {
|
||||
|
@ -515,7 +515,7 @@ class _QRViewState extends State<InvenTreeQRView> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_handler.getOverlayText(context)),
|
||||
title: Text(L10().scanBarcode),
|
||||
),
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
|
@ -13,7 +13,20 @@ class InvenTreeCompany extends InvenTreeModel {
|
||||
String NAME = "Company";
|
||||
|
||||
@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();
|
||||
|
||||
@ -49,7 +62,7 @@ class InvenTreeCompany extends InvenTreeModel {
|
||||
*/
|
||||
class InvenTreeSupplierPart extends InvenTreeModel {
|
||||
@override
|
||||
String URL = "company/part/";
|
||||
String get URL => "company/part/";
|
||||
|
||||
Map<String, String> _filters() {
|
||||
return {
|
||||
|
@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../l10.dart';
|
||||
import '../api_form.dart';
|
||||
|
||||
|
||||
// Paginated response object
|
||||
@ -37,7 +38,7 @@ class InvenTreePageResponse {
|
||||
class InvenTreeModel {
|
||||
|
||||
// Override the endpoint URL for each subclass
|
||||
String URL = "";
|
||||
String get URL => "";
|
||||
|
||||
// Override the web URL for each subclass
|
||||
// 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
|
||||
Map<String, dynamic> jsondata = {};
|
||||
|
||||
@ -155,17 +199,19 @@ class InvenTreeModel {
|
||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
||||
|
||||
// Report error
|
||||
await sentryReportMessage(
|
||||
"InvenTreeModel.reload() returned invalid response",
|
||||
context: {
|
||||
"url": url,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"data": response.data?.toString() ?? "null",
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
}
|
||||
);
|
||||
if (response.statusCode > 0) {
|
||||
await sentryReportMessage(
|
||||
"InvenTreeModel.reload() returned invalid response",
|
||||
context: {
|
||||
"url": url,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"data": response.data?.toString() ?? "null",
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
showServerError(
|
||||
L10().serverError,
|
||||
@ -226,17 +272,19 @@ class InvenTreeModel {
|
||||
|
||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
||||
|
||||
await sentryReportMessage(
|
||||
"InvenTreeModel.get() returned invalid response",
|
||||
context: {
|
||||
"url": url,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"data": response.data?.toString() ?? "null",
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
}
|
||||
);
|
||||
if (response.statusCode > 0) {
|
||||
await sentryReportMessage(
|
||||
"InvenTreeModel.get() returned invalid response",
|
||||
context: {
|
||||
"url": url,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"data": response.data?.toString() ?? "null",
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
showServerError(
|
||||
L10().serverError,
|
||||
@ -267,17 +315,19 @@ class InvenTreeModel {
|
||||
// Invalid response returned from server
|
||||
if (!response.isValid() || response.data == null || !(response.data is Map)) {
|
||||
|
||||
await sentryReportMessage(
|
||||
"InvenTreeModel.create() returned invalid response",
|
||||
context: {
|
||||
"url": url,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"data": response.data?.toString() ?? "null",
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
}
|
||||
);
|
||||
if (response.statusCode > 0) {
|
||||
await sentryReportMessage(
|
||||
"InvenTreeModel.create() returned invalid response",
|
||||
context: {
|
||||
"url": url,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"data": response.data?.toString() ?? "null",
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
showServerError(
|
||||
L10().serverError,
|
||||
|
@ -14,7 +14,17 @@ class InvenTreePartCategory extends InvenTreeModel {
|
||||
String NAME = "PartCategory";
|
||||
|
||||
@override
|
||||
String URL = "part/category/";
|
||||
String get URL => "part/category/";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
|
||||
return {
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {}
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
@ -68,7 +78,7 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
|
||||
String NAME = "PartTestTemplate";
|
||||
|
||||
@override
|
||||
String URL = "part/test-template/";
|
||||
String get URL => "part/test-template/";
|
||||
|
||||
String get key => jsondata['key'] ?? '';
|
||||
|
||||
@ -125,7 +135,33 @@ class InvenTreePart extends InvenTreeModel {
|
||||
String NAME = "Part";
|
||||
|
||||
@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
|
||||
Map<String, String> defaultListFilters() {
|
||||
|
@ -18,7 +18,7 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
|
||||
String NAME = "StockItemTestResult";
|
||||
|
||||
@override
|
||||
String URL = "stock/test/";
|
||||
String get URL => "stock/test/";
|
||||
|
||||
String get key => jsondata['key'] ?? '';
|
||||
|
||||
@ -103,11 +103,24 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
String NAME = "StockItem";
|
||||
|
||||
@override
|
||||
String URL = "stock/";
|
||||
String get URL => "stock/";
|
||||
|
||||
@override
|
||||
String WEB_URL = "stock/item/";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
return {
|
||||
"part": {},
|
||||
"location": {},
|
||||
"quantity": {},
|
||||
"status": {},
|
||||
"batch": {},
|
||||
"packaging": {},
|
||||
"link": {},
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultGetFilters() {
|
||||
|
||||
@ -544,10 +557,19 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
||||
String NAME = "StockLocation";
|
||||
|
||||
@override
|
||||
String URL = "stock/location/";
|
||||
String get URL => "stock/location/";
|
||||
|
||||
String get pathstring => jsondata['pathstring'] ?? '';
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
return {
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {},
|
||||
};
|
||||
}
|
||||
|
||||
String get parentpathstring {
|
||||
// TODO - Drive the refactor tractor through this
|
||||
List<String> psplit = pathstring.split('/');
|
||||
|
2
lib/l10n
2
lib/l10n
Submodule lib/l10n updated: c81c1c79d1...f4f7b95c28
@ -19,8 +19,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../api_form.dart';
|
||||
|
||||
class CategoryDisplayWidget extends StatefulWidget {
|
||||
|
||||
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
||||
@ -43,27 +41,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
|
||||
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')) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
@ -81,7 +58,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
}
|
||||
|
||||
void _editCategoryDialog(BuildContext context) {
|
||||
|
||||
final _cat = category;
|
||||
|
||||
// Cannot edit top-level category
|
||||
@ -89,17 +65,12 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
launchApiForm(
|
||||
context,
|
||||
L10().editCategory,
|
||||
_cat.url,
|
||||
{
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {},
|
||||
},
|
||||
modelData: _cat.jsondata,
|
||||
onSuccess: refresh,
|
||||
_cat.editForm(
|
||||
context,
|
||||
L10().editCategory,
|
||||
onSuccess: (data) async {
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -206,12 +177,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
label: L10().parts,
|
||||
),
|
||||
// TODO - Add the "actions" item back in
|
||||
/*
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.wrench),
|
||||
label: L10().actions
|
||||
),
|
||||
*/
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -242,18 +211,103 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
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 = [
|
||||
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;
|
||||
}
|
||||
@ -274,7 +328,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
);
|
||||
case 2:
|
||||
return ListView(
|
||||
children: actionTiles()
|
||||
children: actionTiles(context)
|
||||
);
|
||||
default:
|
||||
return ListView();
|
||||
|
@ -65,21 +65,12 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
||||
|
||||
void editCompany(BuildContext context) async {
|
||||
|
||||
launchApiForm(
|
||||
company.editForm(
|
||||
context,
|
||||
L10().companyEdit,
|
||||
company.url,
|
||||
{
|
||||
"name": {},
|
||||
"description": {},
|
||||
"website": {},
|
||||
"is_supplier": {},
|
||||
"is_manufacturer": {},
|
||||
"is_customer": {},
|
||||
"currency": {},
|
||||
},
|
||||
modelData: company.jsondata,
|
||||
onSuccess: refresh
|
||||
onSuccess: (data) async {
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/api_form.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/app_settings.dart';
|
||||
import 'package:inventree/barcode.dart';
|
||||
@ -84,17 +83,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
launchApiForm(
|
||||
_loc.editForm(
|
||||
context,
|
||||
L10().editLocation,
|
||||
_loc.url,
|
||||
{
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {},
|
||||
},
|
||||
modelData: _loc.jsondata,
|
||||
onSuccess: refresh
|
||||
onSuccess: (data) async {
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -142,6 +136,61 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
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}) {
|
||||
if (location == null) {
|
||||
return Card(
|
||||
@ -206,7 +255,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
icon: FaIcon(FontAwesomeIcons.boxes),
|
||||
label: L10().stock,
|
||||
),
|
||||
// TODO - Add in actions when they are written...
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.wrench),
|
||||
label: L10().actions,
|
||||
@ -283,6 +331,32 @@ List<Widget> detailTiles() {
|
||||
|
||||
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) {
|
||||
// Stock adjustment actions
|
||||
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
||||
|
@ -6,12 +6,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import 'package:inventree/api_form.dart';
|
||||
import 'package:inventree/widget/part_notes.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/category_display.dart';
|
||||
import 'package:inventree/widget/part_suppliers.dart';
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/part_image_widget.dart';
|
||||
@ -100,33 +98,12 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
|
||||
void _editPartDialog(BuildContext context) {
|
||||
|
||||
launchApiForm(
|
||||
context,
|
||||
L10().editPart,
|
||||
part.url,
|
||||
{
|
||||
"name": {},
|
||||
"description": {},
|
||||
"IPN": {},
|
||||
"revision": {},
|
||||
"keywords": {},
|
||||
"link": {},
|
||||
|
||||
"category": {
|
||||
},
|
||||
|
||||
// Checkbox fields
|
||||
"active": {},
|
||||
"assembly": {},
|
||||
"component": {},
|
||||
"purchaseable": {},
|
||||
"salable": {},
|
||||
"trackable": {},
|
||||
"is_template": {},
|
||||
"virtual": {},
|
||||
},
|
||||
modelData: part.jsondata,
|
||||
onSuccess: refresh,
|
||||
part.editForm(
|
||||
context,
|
||||
L10().editPart,
|
||||
onSuccess: (data) async {
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -305,16 +282,18 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
// Tiles for an "assembly" part
|
||||
if (part.isAssembly) {
|
||||
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().billOfMaterials),
|
||||
leading: FaIcon(FontAwesomeIcons.thList),
|
||||
trailing: Text("${part.bomItemCount}"),
|
||||
onTap: () {
|
||||
// TODO
|
||||
}
|
||||
)
|
||||
);
|
||||
if (part.bomItemCount > 0) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().billOfMaterials),
|
||||
leading: FaIcon(FontAwesomeIcons.thList),
|
||||
trailing: Text("${part.bomItemCount}"),
|
||||
onTap: () {
|
||||
// TODO
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (part.building > 0) {
|
||||
tiles.add(
|
||||
@ -331,7 +310,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
}
|
||||
|
||||
// Tiles for "component" part
|
||||
if (part.isComponent) {
|
||||
if (part.isComponent && part.usedInCount > 0) {
|
||||
|
||||
tiles.add(
|
||||
ListTile(
|
||||
@ -421,6 +400,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;
|
||||
}
|
||||
@ -469,12 +461,10 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
label: L10().stock
|
||||
),
|
||||
// TODO - Add part actions
|
||||
/*
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.wrench),
|
||||
label: L10().actions,
|
||||
),
|
||||
*/
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -7,8 +7,6 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import '../api_form.dart';
|
||||
|
||||
|
||||
class PartNotesWidget extends StatefulWidget {
|
||||
|
||||
@ -46,17 +44,15 @@ class _PartNotesState extends RefreshableState<PartNotesWidget> {
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
tooltip: L10().edit,
|
||||
onPressed: () {
|
||||
launchApiForm(
|
||||
part.editForm(
|
||||
context,
|
||||
L10().editNotes,
|
||||
part.url,
|
||||
{
|
||||
fields: {
|
||||
"notes": {
|
||||
"multiline": true,
|
||||
}
|
||||
},
|
||||
modelData: part.jsondata,
|
||||
onSuccess: () async {
|
||||
onSuccess: (data) async {
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
|
@ -22,8 +22,6 @@ import 'package:inventree/api.dart';
|
||||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
import '../api_form.dart';
|
||||
|
||||
class StockDetailWidget extends StatefulWidget {
|
||||
|
||||
StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
||||
@ -109,18 +107,20 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
|
||||
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,
|
||||
L10().editItem,
|
||||
item.url,
|
||||
{
|
||||
"status": {},
|
||||
"batch": {},
|
||||
"packaging": {},
|
||||
"link": {},
|
||||
},
|
||||
modelData: item.jsondata,
|
||||
onSuccess: refresh
|
||||
fields: fields,
|
||||
onSuccess: (data) async {
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import '../api.dart';
|
||||
import '../api_form.dart';
|
||||
|
||||
|
||||
class StockNotesWidget extends StatefulWidget {
|
||||
@ -46,19 +45,17 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> {
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
tooltip: L10().edit,
|
||||
onPressed: () {
|
||||
launchApiForm(
|
||||
context,
|
||||
L10().editNotes,
|
||||
item.url,
|
||||
{
|
||||
"notes": {
|
||||
"multiline": true,
|
||||
}
|
||||
},
|
||||
modelData: item.jsondata,
|
||||
onSuccess: () {
|
||||
refresh();
|
||||
item.editForm(
|
||||
context,
|
||||
L10().editNotes,
|
||||
fields: {
|
||||
"notes": {
|
||||
"multiline": true,
|
||||
}
|
||||
},
|
||||
onSuccess: (data) async {
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
|
@ -1,6 +1,5 @@
|
||||
|
||||
|
||||
import 'package:email_validator/email_validator.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
Reference in New Issue
Block a user