mirror of
https://github.com/inventree/inventree-app.git
synced 2025-12-03 18:59:50 +00:00
Modern Label printing (#724)
* Basic widget * Redirect label printing action * Refactor label printing code * Construct form elements * Refactor to allow re-use of forms * Basic rendering of label printing form * Remove dead code * Pass custom handler through to form context * Refactoring API forms: - Allow custom pk field name - Add callback when values change * linting * Dynamically rebuild form * Handle nested fields * Handle label printing status * Run dart format * Update release notes * Remove unused var * Enable close icon * Handle initial plugin default value * Store default values: - Selected template (per label type) - Selected printing plugin * Dart format * Fix dart linting * use setter * Just use a public field
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
### 0.21.0 - November 2025
|
### 0.21.0 - November 2025
|
||||||
---
|
---
|
||||||
|
|
||||||
|
- Support label printing again, fixing issues with new printing API
|
||||||
- Adds zoom controller for barcode scanner camera view
|
- Adds zoom controller for barcode scanner camera view
|
||||||
- Display default stock location in Part detail page
|
- Display default stock location in Part detail page
|
||||||
- Display stock information in SupplierPart detail page
|
- Display stock information in SupplierPart detail page
|
||||||
|
|||||||
17
lib/api.dart
17
lib/api.dart
@@ -7,6 +7,7 @@ import "package:http/http.dart" as http;
|
|||||||
import "package:http/io_client.dart";
|
import "package:http/io_client.dart";
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import "package:inventree/main.dart";
|
import "package:inventree/main.dart";
|
||||||
|
import "package:inventree/widget/progress.dart";
|
||||||
import "package:one_context/one_context.dart";
|
import "package:one_context/one_context.dart";
|
||||||
import "package:open_filex/open_filex.dart";
|
import "package:open_filex/open_filex.dart";
|
||||||
import "package:cached_network_image/cached_network_image.dart";
|
import "package:cached_network_image/cached_network_image.dart";
|
||||||
@@ -912,6 +913,8 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
var client = createClient(url, strictHttps: strictHttps);
|
var client = createClient(url, strictHttps: strictHttps);
|
||||||
|
|
||||||
|
showLoadingOverlay();
|
||||||
|
|
||||||
// Attempt to open a connection to the server
|
// Attempt to open a connection to the server
|
||||||
try {
|
try {
|
||||||
_request = await client
|
_request = await client
|
||||||
@@ -953,6 +956,7 @@ class InvenTreeAPI {
|
|||||||
await localFile.writeAsBytes(bytes);
|
await localFile.writeAsBytes(bytes);
|
||||||
|
|
||||||
if (openOnDownload) {
|
if (openOnDownload) {
|
||||||
|
hideLoadingOverlay();
|
||||||
OpenFilex.open(local_path);
|
OpenFilex.open(local_path);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -972,6 +976,8 @@ class InvenTreeAPI {
|
|||||||
stackTrace,
|
stackTrace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideLoadingOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1085,8 +1091,15 @@ class InvenTreeAPI {
|
|||||||
* We send this with the currently selected "locale",
|
* We send this with the currently selected "locale",
|
||||||
* so that (hopefully) the field messages are correctly translated
|
* so that (hopefully) the field messages are correctly translated
|
||||||
*/
|
*/
|
||||||
Future<APIResponse> options(String url) async {
|
Future<APIResponse> options(
|
||||||
HttpClientRequest? request = await apiRequest(url, "OPTIONS");
|
String url, {
|
||||||
|
Map<String, String> params = const {},
|
||||||
|
}) async {
|
||||||
|
HttpClientRequest? request = await apiRequest(
|
||||||
|
url,
|
||||||
|
"OPTIONS",
|
||||||
|
urlParams: params,
|
||||||
|
);
|
||||||
|
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
// Return an "invalid" APIResponse
|
// Return an "invalid" APIResponse
|
||||||
|
|||||||
@@ -26,23 +26,52 @@ import "package:inventree/widget/fields.dart";
|
|||||||
import "package:inventree/widget/progress.dart";
|
import "package:inventree/widget/progress.dart";
|
||||||
import "package:inventree/widget/snacks.dart";
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract field options from a returned OPTIONS request
|
||||||
|
*/
|
||||||
|
Map<String, dynamic> extractFields(APIResponse response) {
|
||||||
|
if (!response.isValid()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = response.asMap();
|
||||||
|
|
||||||
|
if (!data.containsKey("actions")) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions = response.data["actions"] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
dynamic result = actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {};
|
||||||
|
|
||||||
|
return result as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Class that represents a single "form field",
|
* Class that represents a single "form field",
|
||||||
* defined by the InvenTree API
|
* defined by the InvenTree API
|
||||||
*/
|
*/
|
||||||
class APIFormField {
|
class APIFormField {
|
||||||
// Constructor
|
// Constructor
|
||||||
APIFormField(this.name, this.data);
|
APIFormField(this.name, this.data, {this.formHandler});
|
||||||
|
|
||||||
// File to be uploaded for this filed
|
// File to be uploaded for this filed
|
||||||
File? attachedfile;
|
File? attachedfile;
|
||||||
|
|
||||||
|
APIFormWidgetState? formHandler;
|
||||||
|
|
||||||
// Name of this field
|
// Name of this field
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
// JSON data which defines the field
|
// JSON data which defines the field
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
|
|
||||||
|
// Function to update the value of this field
|
||||||
|
void setFieldValue(dynamic val) {
|
||||||
|
data["value"] = val;
|
||||||
|
formHandler?.onValueChanged(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
// JSON field definition provided by the server
|
// JSON field definition provided by the server
|
||||||
Map<String, dynamic> definition = {};
|
Map<String, dynamic> definition = {};
|
||||||
|
|
||||||
@@ -88,6 +117,8 @@ class APIFormField {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get pk_field => (getParameter("pk_field") ?? "pk") as String;
|
||||||
|
|
||||||
// Get the "api_url" associated with a related field
|
// Get the "api_url" associated with a related field
|
||||||
String get api_url => (getParameter("api_url") ?? "") as String;
|
String get api_url => (getParameter("api_url") ?? "") as String;
|
||||||
|
|
||||||
@@ -244,18 +275,13 @@ class APIFormField {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? pk = int.tryParse(value.toString());
|
String url = api_url + "/" + value.toString() + "/";
|
||||||
|
|
||||||
if (pk == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String url = api_url + "/" + pk.toString() + "/";
|
|
||||||
|
|
||||||
final APIResponse response = await InvenTreeAPI().get(url, params: filters);
|
final APIResponse response = await InvenTreeAPI().get(url, params: filters);
|
||||||
|
|
||||||
if (response.successful()) {
|
if (response.successful()) {
|
||||||
initial_data = response.data;
|
initial_data = response.data;
|
||||||
|
formHandler?.onValueChanged(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +295,7 @@ class APIFormField {
|
|||||||
return _constructBoolean();
|
return _constructBoolean();
|
||||||
case "related field":
|
case "related field":
|
||||||
return _constructRelatedField();
|
return _constructRelatedField();
|
||||||
|
case "integer":
|
||||||
case "float":
|
case "float":
|
||||||
case "decimal":
|
case "decimal":
|
||||||
return _constructFloatField();
|
return _constructFloatField();
|
||||||
@@ -318,8 +345,7 @@ class APIFormField {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var handler = UniqueBarcodeHandler((String hash) {
|
var handler = UniqueBarcodeHandler((String hash) {
|
||||||
controller.text = hash;
|
controller.text = hash;
|
||||||
data["value"] = hash;
|
setFieldValue(hash);
|
||||||
|
|
||||||
barcodeSuccess(L10().barcodeAssigned);
|
barcodeSuccess(L10().barcodeAssigned);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,9 +373,9 @@ class APIFormField {
|
|||||||
onChanged: (DateTime? time) {
|
onChanged: (DateTime? time) {
|
||||||
// Save the time string
|
// Save the time string
|
||||||
if (time == null) {
|
if (time == null) {
|
||||||
data["value"] = null;
|
setFieldValue(null);
|
||||||
} else {
|
} else {
|
||||||
data["value"] = time.toString().split(" ").first;
|
setFieldValue(time.toString().split(" ").first);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onShowPicker: (context, value) async {
|
onShowPicker: (context, value) async {
|
||||||
@@ -432,9 +458,9 @@ class APIFormField {
|
|||||||
},
|
},
|
||||||
onSaved: (item) {
|
onSaved: (item) {
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
data["value"] = null;
|
setFieldValue(null);
|
||||||
} else {
|
} else {
|
||||||
data["value"] = item["value"];
|
setFieldValue(item["value"]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -481,7 +507,7 @@ class APIFormField {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onSaved: (val) {
|
onSaved: (val) {
|
||||||
data["value"] = val;
|
setFieldValue(val);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -527,7 +553,20 @@ class APIFormField {
|
|||||||
hintText: helpText,
|
hintText: helpText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: null,
|
onChanged: (item) {
|
||||||
|
if (item != null) {
|
||||||
|
setFieldValue(item[pk_field]);
|
||||||
|
} else {
|
||||||
|
setFieldValue(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSaved: (item) {
|
||||||
|
if (item != null) {
|
||||||
|
setFieldValue(item[pk_field]);
|
||||||
|
} else {
|
||||||
|
setFieldValue(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
itemAsString: (dynamic item) {
|
itemAsString: (dynamic item) {
|
||||||
Map<String, dynamic> data = item as Map<String, dynamic>;
|
Map<String, dynamic> data = item as Map<String, dynamic>;
|
||||||
|
|
||||||
@@ -551,13 +590,6 @@ class APIFormField {
|
|||||||
dropdownBuilder: (context, item) {
|
dropdownBuilder: (context, item) {
|
||||||
return _renderRelatedField(name, item, true, false);
|
return _renderRelatedField(name, item, true, false);
|
||||||
},
|
},
|
||||||
onSaved: (item) {
|
|
||||||
if (item != null) {
|
|
||||||
data["value"] = item["pk"];
|
|
||||||
} else {
|
|
||||||
data["value"] = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
compareFn: (dynamic item, dynamic selectedItem) {
|
compareFn: (dynamic item, dynamic selectedItem) {
|
||||||
// Comparison is based on the PK value
|
// Comparison is based on the PK value
|
||||||
|
|
||||||
@@ -568,7 +600,8 @@ class APIFormField {
|
|||||||
bool result = false;
|
bool result = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = item["pk"].toString() == selectedItem["pk"].toString();
|
result =
|
||||||
|
item[pk_field].toString() == selectedItem[pk_field].toString();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Catch any conversion errors
|
// Catch any conversion errors
|
||||||
result = false;
|
result = false;
|
||||||
@@ -765,6 +798,18 @@ class APIFormField {
|
|||||||
so.customer?.thumbnail ?? so.customer?.image ?? "",
|
so.customer?.thumbnail ?? so.customer?.image ?? "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
case "labeltemplate":
|
||||||
|
return ListTile(
|
||||||
|
title: Text((data["name"] ?? "").toString()),
|
||||||
|
subtitle: Text((data["description"] ?? "").toString()),
|
||||||
|
);
|
||||||
|
case "pluginconfig":
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
(data["meta"]?["human_name"] ?? data["name"] ?? "").toString(),
|
||||||
|
),
|
||||||
|
subtitle: Text((data["meta"]?["description"] ?? "").toString()),
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -810,8 +855,11 @@ class APIFormField {
|
|||||||
maxLines: multiline ? null : 1,
|
maxLines: multiline ? null : 1,
|
||||||
expands: false,
|
expands: false,
|
||||||
initialValue: (value ?? "") as String,
|
initialValue: (value ?? "") as String,
|
||||||
|
onChanged: (val) {
|
||||||
|
setFieldValue(val);
|
||||||
|
},
|
||||||
onSaved: (val) {
|
onSaved: (val) {
|
||||||
data["value"] = val;
|
setFieldValue(val);
|
||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (required && (value == null || value.isEmpty)) {
|
if (required && (value == null || value.isEmpty)) {
|
||||||
@@ -842,7 +890,7 @@ class APIFormField {
|
|||||||
initial: initial_value,
|
initial: initial_value,
|
||||||
tristate: (getParameter("tristate") ?? false) as bool,
|
tristate: (getParameter("tristate") ?? false) as bool,
|
||||||
onSaved: (val) {
|
onSaved: (val) {
|
||||||
data["value"] = val;
|
setFieldValue(val);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -865,27 +913,6 @@ class APIFormField {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Extract field options from a returned OPTIONS request
|
|
||||||
*/
|
|
||||||
Map<String, dynamic> extractFields(APIResponse response) {
|
|
||||||
if (!response.isValid()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = response.asMap();
|
|
||||||
|
|
||||||
if (!data.containsKey("actions")) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
var actions = response.data["actions"] as Map<String, dynamic>;
|
|
||||||
|
|
||||||
dynamic result = actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {};
|
|
||||||
|
|
||||||
return result as Map<String, dynamic>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Extract a field definition (map) from the provided JSON data.
|
* Extract a field definition (map) from the provided JSON data.
|
||||||
*
|
*
|
||||||
@@ -981,6 +1008,7 @@ Future<void> launchApiForm(
|
|||||||
Function(Map<String, dynamic>)? onSuccess,
|
Function(Map<String, dynamic>)? onSuccess,
|
||||||
bool Function(Map<String, dynamic>)? validate,
|
bool Function(Map<String, dynamic>)? validate,
|
||||||
Function? onCancel,
|
Function? onCancel,
|
||||||
|
APIFormWidgetState? formHandler,
|
||||||
IconData icon = TablerIcons.device_floppy,
|
IconData icon = TablerIcons.device_floppy,
|
||||||
}) async {
|
}) async {
|
||||||
showLoadingOverlay();
|
showLoadingOverlay();
|
||||||
@@ -1041,7 +1069,7 @@ Future<void> launchApiForm(
|
|||||||
field.data["instance_value"] = model_value;
|
field.data["instance_value"] = model_value;
|
||||||
|
|
||||||
if (field.data["value"] == null) {
|
if (field.data["value"] == null) {
|
||||||
field.data["value"] = model_value;
|
field.setFieldValue(model_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
formFields.add(field);
|
formFields.add(field);
|
||||||
@@ -1066,6 +1094,7 @@ Future<void> launchApiForm(
|
|||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
validate: validate,
|
validate: validate,
|
||||||
fileField: fileField,
|
fileField: fileField,
|
||||||
|
state: formHandler,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1079,6 +1108,7 @@ class APIFormWidget extends StatefulWidget {
|
|||||||
this.fields,
|
this.fields,
|
||||||
this.method, {
|
this.method, {
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.state,
|
||||||
this.onSuccess,
|
this.onSuccess,
|
||||||
this.validate,
|
this.validate,
|
||||||
this.fileField = "",
|
this.fileField = "",
|
||||||
@@ -1105,12 +1135,15 @@ class APIFormWidget extends StatefulWidget {
|
|||||||
|
|
||||||
final bool Function(Map<String, dynamic>)? validate;
|
final bool Function(Map<String, dynamic>)? validate;
|
||||||
|
|
||||||
|
final APIFormWidgetState? state;
|
||||||
|
|
||||||
|
// Default form handler is constructed if none is provided
|
||||||
@override
|
@override
|
||||||
_APIFormWidgetState createState() => _APIFormWidgetState();
|
APIFormWidgetState createState() => state ?? APIFormWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _APIFormWidgetState extends State<APIFormWidget> {
|
class APIFormWidgetState extends State<APIFormWidget> {
|
||||||
_APIFormWidgetState() : super();
|
APIFormWidgetState() : super();
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
@@ -1118,6 +1151,33 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
|
|
||||||
bool spacerRequired = false;
|
bool spacerRequired = false;
|
||||||
|
|
||||||
|
// Return a list of all fields used for this form
|
||||||
|
// The default implementation just returns the fields provided to the widget
|
||||||
|
// However, custom form implementations may override this function
|
||||||
|
List<APIFormField> get formFields {
|
||||||
|
final List<APIFormField> fields = widget.fields;
|
||||||
|
|
||||||
|
// Ensure each field has access to this form handler
|
||||||
|
for (var field in fields) {
|
||||||
|
field.formHandler ??= this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback for when a field value is changed
|
||||||
|
// Default implementation does nothing,
|
||||||
|
// but custom form implementations may override this function
|
||||||
|
void onValueChanged(String field, dynamic value) {}
|
||||||
|
|
||||||
|
Future<void> handleSuccess(
|
||||||
|
Map<String, dynamic> submittedData,
|
||||||
|
Map<String, dynamic> responseData,
|
||||||
|
) async {
|
||||||
|
widget.onSuccess?.call(responseData);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> _buildForm() {
|
List<Widget> _buildForm() {
|
||||||
List<Widget> widgets = [];
|
List<Widget> widgets = [];
|
||||||
|
|
||||||
@@ -1135,7 +1195,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
widgets.add(Divider(height: 5));
|
widgets.add(Divider(height: 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var field in widget.fields) {
|
for (var field in formFields) {
|
||||||
if (field.hidden) {
|
if (field.hidden) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1190,7 +1250,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
// Pop the "file" field
|
// Pop the "file" field
|
||||||
data.remove(widget.fileField);
|
data.remove(widget.fileField);
|
||||||
|
|
||||||
for (var field in widget.fields) {
|
for (var field in formFields) {
|
||||||
if (field.name == widget.fileField) {
|
if (field.name == widget.fileField) {
|
||||||
File? file = field.attachedfile;
|
File? file = field.attachedfile;
|
||||||
|
|
||||||
@@ -1275,7 +1335,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
match = true;
|
match = true;
|
||||||
continue;
|
continue;
|
||||||
default:
|
default:
|
||||||
for (var field in widget.fields) {
|
for (var field in formFields) {
|
||||||
// Hidden fields can't display errors, so we won't match
|
// Hidden fields can't display errors, so we won't match
|
||||||
if (field.hidden) {
|
if (field.hidden) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1327,7 +1387,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
|
|
||||||
// Iterate through and find "simple" top-level fields
|
// Iterate through and find "simple" top-level fields
|
||||||
|
|
||||||
for (var field in widget.fields) {
|
for (var field in formFields) {
|
||||||
if (field.readOnly) {
|
if (field.readOnly) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1366,20 +1426,11 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run custom onSuccess function
|
|
||||||
var successFunc = widget.onSuccess;
|
|
||||||
|
|
||||||
// An "empty" URL means we don't want to submit the form anywhere
|
// An "empty" URL means we don't want to submit the form anywhere
|
||||||
// Perhaps we just want to process the data?
|
// Perhaps we just want to process the data?
|
||||||
if (widget.url.isEmpty) {
|
if (widget.url.isEmpty) {
|
||||||
// Hide the form
|
// Hide the form
|
||||||
Navigator.pop(context);
|
handleSuccess(data, {});
|
||||||
|
|
||||||
if (successFunc != null) {
|
|
||||||
// Return the raw "submitted" data, rather than the server response
|
|
||||||
successFunc(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1394,29 +1445,24 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
case 200:
|
case 200:
|
||||||
case 201:
|
case 201:
|
||||||
// Form was successfully validated by the server
|
// Form was successfully validated by the server
|
||||||
|
// Ensure the response is a valid JSON structure
|
||||||
|
Map<String, dynamic> json = {};
|
||||||
|
|
||||||
// Hide this form
|
var responseData = response.asMap();
|
||||||
Navigator.pop(context);
|
|
||||||
|
|
||||||
if (successFunc != null) {
|
for (String key in responseData.keys) {
|
||||||
// Ensure the response is a valid JSON structure
|
json[key.toString()] = responseData[key];
|
||||||
Map<String, dynamic> json = {};
|
|
||||||
|
|
||||||
var data = response.asMap();
|
|
||||||
|
|
||||||
for (String key in data.keys) {
|
|
||||||
json[key.toString()] = data[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
successFunc(json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSuccess(data, json);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case 400:
|
case 400:
|
||||||
// Form submission / validation error
|
// Form submission / validation error
|
||||||
showSnackIcon(L10().formError, success: false);
|
showSnackIcon(L10().formError, success: false);
|
||||||
|
|
||||||
// Update field errors
|
// Update field errors
|
||||||
for (var field in widget.fields) {
|
for (var field in formFields) {
|
||||||
field.extractErrorMessages(response);
|
field.extractErrorMessages(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1444,6 +1490,22 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct the internal form widget, based on the provided fields
|
||||||
|
Widget buildForm(BuildContext context) {
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _buildForm(),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -1463,18 +1525,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Form(
|
body: buildForm(context),
|
||||||
key: _formKey,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: _buildForm(),
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -695,6 +695,12 @@
|
|||||||
"keywords": "Keywords",
|
"keywords": "Keywords",
|
||||||
"@keywords": {},
|
"@keywords": {},
|
||||||
|
|
||||||
|
"labelDriver": "Label Driver",
|
||||||
|
"@labelDriver": {},
|
||||||
|
|
||||||
|
"labelSelectDriver": "Select Label Printer Driver",
|
||||||
|
"@labelSelectDriver": {},
|
||||||
|
|
||||||
"labelPrinting": "Label Printing",
|
"labelPrinting": "Label Printing",
|
||||||
"@labelPrinting": {},
|
"@labelPrinting": {},
|
||||||
|
|
||||||
|
|||||||
422
lib/labels.dart
422
lib/labels.dart
@@ -1,11 +1,206 @@
|
|||||||
import "package:flutter/cupertino.dart";
|
import "package:flutter/cupertino.dart";
|
||||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/widget/progress.dart";
|
import "package:inventree/preferences.dart";
|
||||||
import "package:inventree/api_form.dart";
|
import "package:inventree/api_form.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/widget/progress.dart";
|
||||||
import "package:inventree/widget/snacks.dart";
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
|
||||||
|
const String PRINT_LABEL_URL = "api/label/print/";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Custom form handler for label printing.
|
||||||
|
* Required to manage dynamic form fields.
|
||||||
|
*/
|
||||||
|
class LabelFormWidgetState extends APIFormWidgetState {
|
||||||
|
LabelFormWidgetState() : super();
|
||||||
|
|
||||||
|
List<APIFormField> dynamicFields = [];
|
||||||
|
|
||||||
|
String pluginKey = "";
|
||||||
|
String labelType = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<APIFormField> get formFields {
|
||||||
|
final baseFields = super.formFields;
|
||||||
|
|
||||||
|
if (pluginKey.isEmpty) {
|
||||||
|
// Handle case where default plugin is provided
|
||||||
|
final APIFormField pluginField = baseFields.firstWhere(
|
||||||
|
(field) => field.name == "plugin",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pluginField.initial_data != null) {
|
||||||
|
pluginKey = pluginField.value.toString();
|
||||||
|
onValueChanged("plugin", pluginKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...baseFields, ...dynamicFields];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onValueChanged(String field, dynamic value) {
|
||||||
|
if (field == "plugin") {
|
||||||
|
onPluginChanged(value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> handleSuccess(
|
||||||
|
Map<String, dynamic> submittedData,
|
||||||
|
Map<String, dynamic> responseData,
|
||||||
|
) async {
|
||||||
|
super.handleSuccess(submittedData, responseData);
|
||||||
|
|
||||||
|
// Save default values to the database
|
||||||
|
final String? plugin = submittedData["plugin"]?.toString();
|
||||||
|
final int? template = submittedData["template"] as int?;
|
||||||
|
|
||||||
|
// Save default printing plugin
|
||||||
|
if (plugin != null) {
|
||||||
|
InvenTreeSettingsManager().setValue(INV_LABEL_DEFAULT_PLUGIN, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save default template for this label type
|
||||||
|
if (labelType.isNotEmpty && template != null) {
|
||||||
|
final defaultTemplates =
|
||||||
|
await InvenTreeSettingsManager().getValue(
|
||||||
|
INV_LABEL_DEFAULT_TEMPLATES,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
InvenTreeSettingsManager().setValue(INV_LABEL_DEFAULT_TEMPLATES, {
|
||||||
|
...?defaultTemplates,
|
||||||
|
labelType: template,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Re-fetch printing options when the plugin changes
|
||||||
|
*/
|
||||||
|
Future<void> onPluginChanged(String key) async {
|
||||||
|
showLoadingOverlay();
|
||||||
|
|
||||||
|
InvenTreeAPI().options(PRINT_LABEL_URL, params: {"plugin": key}).then((
|
||||||
|
APIResponse response,
|
||||||
|
) {
|
||||||
|
if (response.isValid()) {
|
||||||
|
updateFields(response);
|
||||||
|
hideLoadingOverlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback when the server responds with printing options,
|
||||||
|
* based on the selected printing plugin
|
||||||
|
*/
|
||||||
|
Future<void> updateFields(APIResponse response) async {
|
||||||
|
Map<String, dynamic> printingFields = extractFields(response);
|
||||||
|
|
||||||
|
// Find only the fields which are not in the "base" fields
|
||||||
|
List<APIFormField> uniqueFields = [];
|
||||||
|
|
||||||
|
for (String key in printingFields.keys) {
|
||||||
|
if (super.formFields.any((field) => field.name == key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic data = printingFields[key];
|
||||||
|
|
||||||
|
Map<String, dynamic> fieldData = {};
|
||||||
|
|
||||||
|
if (data is Map) {
|
||||||
|
fieldData = Map<String, dynamic>.from(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
APIFormField field = APIFormField(key, fieldData);
|
||||||
|
field.definition = extractFieldDefinition(
|
||||||
|
printingFields,
|
||||||
|
field.lookupPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (field.type == "dependent field") {
|
||||||
|
// Dependent fields must be handled separately
|
||||||
|
|
||||||
|
// TODO: This should be refactored into api_form.dart
|
||||||
|
dynamic child = field.definition["child"];
|
||||||
|
|
||||||
|
if (child != null && child is Map) {
|
||||||
|
Map<String, dynamic> child_map = child as Map<String, dynamic>;
|
||||||
|
dynamic nested_children = child_map["children"];
|
||||||
|
|
||||||
|
if (nested_children != null && nested_children is Map) {
|
||||||
|
Map<String, dynamic> nested_child_map =
|
||||||
|
nested_children as Map<String, dynamic>;
|
||||||
|
|
||||||
|
for (var field_key in nested_child_map.keys) {
|
||||||
|
field = APIFormField(field_key, nested_child_map);
|
||||||
|
field.definition = extractFieldDefinition(
|
||||||
|
nested_child_map,
|
||||||
|
field_key,
|
||||||
|
);
|
||||||
|
uniqueFields.add(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is a "standard" (non-nested) field
|
||||||
|
uniqueFields.add(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
dynamicFields = uniqueFields;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handlePrintingSuccess(
|
||||||
|
BuildContext context,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
int repeatCount,
|
||||||
|
) async {
|
||||||
|
const int MAX_REPEATS = 60;
|
||||||
|
|
||||||
|
int id = (data["pk"] ?? -1) as int;
|
||||||
|
bool complete = (data["complete"] ?? false) as bool;
|
||||||
|
bool error = data["errors"] != null;
|
||||||
|
String? output = data["output"] as String?;
|
||||||
|
|
||||||
|
if (complete) {
|
||||||
|
if (output != null && output.isNotEmpty) {
|
||||||
|
// An output was generated - we can download it!
|
||||||
|
showSnackIcon(L10().downloading, success: true);
|
||||||
|
InvenTreeAPI().downloadFile(output);
|
||||||
|
} else {
|
||||||
|
// Label was offloaded, likely to an external printer
|
||||||
|
showSnackIcon(L10().printLabelSuccess, success: true);
|
||||||
|
}
|
||||||
|
} else if (error) {
|
||||||
|
showSnackIcon(L10().printLabelFailure, success: false);
|
||||||
|
} else if (repeatCount < MAX_REPEATS && id > 0) {
|
||||||
|
// Printing is not yet complete, but we have a valid output ID
|
||||||
|
Future.delayed(Duration(milliseconds: 1000), () async {
|
||||||
|
// Re-query the printing status
|
||||||
|
InvenTreeAPI().get("data-output/$id/").then((response) {
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
if (response.data is Map<String, dynamic>) {
|
||||||
|
final responseData = response.data as Map<String, dynamic>;
|
||||||
|
handlePrintingSuccess(context, responseData, repeatCount + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Select a particular label, from a provided list of options,
|
* Select a particular label, from a provided list of options,
|
||||||
* and print against the selected instances.
|
* and print against the selected instances.
|
||||||
@@ -13,202 +208,73 @@ import "package:inventree/widget/snacks.dart";
|
|||||||
*/
|
*/
|
||||||
Future<void> selectAndPrintLabel(
|
Future<void> selectAndPrintLabel(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<Map<String, dynamic>> labels,
|
|
||||||
int instanceId,
|
|
||||||
String labelType,
|
String labelType,
|
||||||
String labelQuery,
|
int instanceId,
|
||||||
) async {
|
) async {
|
||||||
if (!InvenTreeAPI().isConnected()) {
|
if (!InvenTreeAPI().isConnected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a list of available plugins which support label printing
|
if (!InvenTreeAPI().supportsModernLabelPrinting) {
|
||||||
var plugins = InvenTreeAPI().getPlugins(mixin: "labels");
|
// Legacy label printing API not supported
|
||||||
|
showSnackIcon("Label printing not supported by server", success: false);
|
||||||
dynamic initial_label;
|
return;
|
||||||
dynamic initial_plugin;
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> label_options = [];
|
|
||||||
List<Map<String, dynamic>> plugin_options = [];
|
|
||||||
|
|
||||||
// Construct list of available label templates
|
|
||||||
for (var label in labels) {
|
|
||||||
String name = (label["name"] ?? "").toString();
|
|
||||||
String description = (label["description"] ?? "").toString();
|
|
||||||
|
|
||||||
if (description.isNotEmpty) {
|
|
||||||
name += " - ${description}";
|
|
||||||
}
|
|
||||||
|
|
||||||
int pk = (label["pk"] ?? -1) as int;
|
|
||||||
|
|
||||||
if (name.isNotEmpty && pk > 0) {
|
|
||||||
label_options.add({"display_name": name, "value": pk});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (label_options.length == 1) {
|
// Fetch default values for label printing
|
||||||
initial_label = label_options.first["value"];
|
|
||||||
|
// Default template
|
||||||
|
final defaultTemplates = await InvenTreeSettingsManager().getValue(
|
||||||
|
INV_LABEL_DEFAULT_TEMPLATES,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
int? defaultTemplate;
|
||||||
|
|
||||||
|
if (defaultTemplates != null && defaultTemplates is Map<String, dynamic>) {
|
||||||
|
defaultTemplate = defaultTemplates[labelType] as int?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct list of available plugins
|
// Default plugin
|
||||||
for (var plugin in plugins) {
|
final defaultPlugin = await InvenTreeSettingsManager().getValue(
|
||||||
plugin_options.add({"display_name": plugin.humanName, "value": plugin.key});
|
INV_LABEL_DEFAULT_PLUGIN,
|
||||||
}
|
null,
|
||||||
|
|
||||||
String selectedPlugin = await InvenTreeAPI().getUserSetting(
|
|
||||||
"LABEL_DEFAULT_PRINTER",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedPlugin.isNotEmpty) {
|
// Specify a default list of fields for printing
|
||||||
initial_plugin = selectedPlugin;
|
// The selected plugin may optionally extend this list of fields dynamically
|
||||||
} else if (plugin_options.length == 1) {
|
Map<String, Map<String, dynamic>> baseFields = {
|
||||||
initial_plugin = plugin_options.first["value"];
|
"template": {
|
||||||
}
|
"default": defaultTemplate,
|
||||||
|
"filters": {
|
||||||
Map<String, dynamic> fields = {
|
"enabled": true,
|
||||||
"label": {
|
"model_type": labelType,
|
||||||
"label": L10().labelTemplate,
|
"items": instanceId.toString(),
|
||||||
"type": "choice",
|
},
|
||||||
"value": initial_label,
|
|
||||||
"choices": label_options,
|
|
||||||
"required": true,
|
|
||||||
},
|
},
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"label": L10().pluginPrinter,
|
"default": defaultPlugin,
|
||||||
"type": "choice",
|
"pk_field": "key",
|
||||||
"value": initial_plugin,
|
"filters": {"enabled": true, "mixin": "labels"},
|
||||||
"choices": plugin_options,
|
},
|
||||||
"required": true,
|
"items": {
|
||||||
|
"hidden": true,
|
||||||
|
"value": [instanceId],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final formHandler = LabelFormWidgetState();
|
||||||
|
formHandler.labelType = labelType;
|
||||||
|
|
||||||
launchApiForm(
|
launchApiForm(
|
||||||
context,
|
context,
|
||||||
L10().printLabel,
|
L10().printLabel,
|
||||||
"",
|
PRINT_LABEL_URL,
|
||||||
fields,
|
baseFields,
|
||||||
icon: TablerIcons.printer,
|
method: "POST",
|
||||||
validate: (Map<String, dynamic> data) {
|
modelData: {"plugin": defaultPlugin, "template": defaultTemplate},
|
||||||
final template = data["label"];
|
formHandler: formHandler,
|
||||||
final plugin = data["plugin"];
|
onSuccess: (data) async {
|
||||||
|
handlePrintingSuccess(context, data, 0);
|
||||||
if (template == null) {
|
|
||||||
showSnackIcon(L10().labelSelectTemplate, success: false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin == null) {
|
|
||||||
showSnackIcon(L10().labelSelectPrinter, success: false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
onSuccess: (Map<String, dynamic> data) async {
|
|
||||||
int labelId = (data["label"] ?? -1) as int;
|
|
||||||
var pluginKey = data["plugin"];
|
|
||||||
|
|
||||||
bool result = false;
|
|
||||||
|
|
||||||
if (labelId != -1 && pluginKey != null) {
|
|
||||||
showLoadingOverlay();
|
|
||||||
|
|
||||||
if (InvenTreeAPI().supportsModernLabelPrinting) {
|
|
||||||
// Modern label printing API uses a POST request to a single API endpoint.
|
|
||||||
await InvenTreeAPI()
|
|
||||||
.post(
|
|
||||||
"/label/print/",
|
|
||||||
body: {
|
|
||||||
"plugin": pluginKey,
|
|
||||||
"template": labelId,
|
|
||||||
"items": [instanceId],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then((APIResponse response) {
|
|
||||||
if (response.isValid() &&
|
|
||||||
response.statusCode >= 200 &&
|
|
||||||
response.statusCode <= 201) {
|
|
||||||
var data = response.asMap();
|
|
||||||
|
|
||||||
if (data.containsKey("output")) {
|
|
||||||
String? label_file = (data["output"]) as String?;
|
|
||||||
|
|
||||||
if (label_file != null && label_file.isNotEmpty) {
|
|
||||||
// Attempt to open generated file
|
|
||||||
InvenTreeAPI().downloadFile(label_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Legacy label printing API
|
|
||||||
// Uses a GET request to a specially formed URL which depends on the parameters
|
|
||||||
String url =
|
|
||||||
"/label/${labelType}/${labelId}/print/?${labelQuery}&plugin=${pluginKey}";
|
|
||||||
await InvenTreeAPI().get(url).then((APIResponse response) {
|
|
||||||
if (response.isValid() && response.statusCode == 200) {
|
|
||||||
var data = response.asMap();
|
|
||||||
if (data.containsKey("file")) {
|
|
||||||
var label_file = (data["file"] ?? "") as String;
|
|
||||||
|
|
||||||
// Attempt to open remote file
|
|
||||||
InvenTreeAPI().downloadFile(label_file);
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hideLoadingOverlay();
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
showSnackIcon(L10().printLabelSuccess, success: true);
|
|
||||||
} else {
|
|
||||||
showSnackIcon(L10().printLabelFailure, success: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Discover which label templates are available for a given item
|
|
||||||
*/
|
|
||||||
Future<List<Map<String, dynamic>>> getLabelTemplates(
|
|
||||||
String labelType,
|
|
||||||
Map<String, String> data,
|
|
||||||
) async {
|
|
||||||
if (!InvenTreeAPI().isConnected() ||
|
|
||||||
!InvenTreeAPI().supportsMixin("labels")) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by active plugins
|
|
||||||
data["enabled"] = "true";
|
|
||||||
|
|
||||||
String url = "/label/template/";
|
|
||||||
|
|
||||||
if (InvenTreeAPI().supportsModernLabelPrinting) {
|
|
||||||
data["model_type"] = labelType;
|
|
||||||
} else {
|
|
||||||
// Legacy label printing API endpoint
|
|
||||||
url = "/label/${labelType}/";
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> labels = [];
|
|
||||||
|
|
||||||
await InvenTreeAPI().get(url, params: data).then((APIResponse response) {
|
|
||||||
if (response.isValid() && response.statusCode == 200) {
|
|
||||||
for (var label in response.resultsList()) {
|
|
||||||
if (label is Map<String, dynamic>) {
|
|
||||||
labels.add(label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ const int SCREEN_ORIENTATION_LANDSCAPE = 2;
|
|||||||
const String INV_SOUNDS_BARCODE = "barcodeSounds";
|
const String INV_SOUNDS_BARCODE = "barcodeSounds";
|
||||||
const String INV_SOUNDS_SERVER = "serverSounds";
|
const String INV_SOUNDS_SERVER = "serverSounds";
|
||||||
|
|
||||||
|
// Label printing settings
|
||||||
const String INV_ENABLE_LABEL_PRINTING = "enableLabelPrinting";
|
const String INV_ENABLE_LABEL_PRINTING = "enableLabelPrinting";
|
||||||
|
const String INV_LABEL_DEFAULT_TEMPLATES = "defaultLabelTemplates";
|
||||||
|
const String INV_LABEL_DEFAULT_PRINTER = "defaultLabelPrinter";
|
||||||
|
const String INV_LABEL_DEFAULT_PLUGIN = "defaultLabelPlugin";
|
||||||
|
|
||||||
// Part settings
|
// Part settings
|
||||||
const String INV_PART_SHOW_PARAMETERS = "partShowParameters";
|
const String INV_PART_SHOW_PARAMETERS = "partShowParameters";
|
||||||
|
|||||||
@@ -63,8 +63,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
|
|
||||||
InvenTreePartPricing? partPricing;
|
InvenTreePartPricing? partPricing;
|
||||||
|
|
||||||
List<Map<String, dynamic>> labels = [];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle() => L10().partDetails;
|
String getAppBarTitle() => L10().partDetails;
|
||||||
|
|
||||||
@@ -121,19 +119,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (labels.isNotEmpty) {
|
if (allowLabelPrinting && api.supportsModernLabelPrinting) {
|
||||||
actions.add(
|
actions.add(
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: Icon(TablerIcons.printer),
|
child: Icon(TablerIcons.printer),
|
||||||
label: L10().printLabel,
|
label: L10().printLabel,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
selectAndPrintLabel(
|
selectAndPrintLabel(context, "part", widget.part.pk);
|
||||||
context,
|
|
||||||
labels,
|
|
||||||
widget.part.pk,
|
|
||||||
"part",
|
|
||||||
"part=${widget.part.pk}",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -271,26 +263,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
List<Map<String, dynamic>> _labels = [];
|
|
||||||
allowLabelPrinting &= api.supportsMixin("labels");
|
|
||||||
|
|
||||||
if (allowLabelPrinting) {
|
|
||||||
String model_type = api.supportsModernLabelPrinting
|
|
||||||
? InvenTreePart.MODEL_TYPE
|
|
||||||
: "part";
|
|
||||||
String item_key = api.supportsModernLabelPrinting ? "items" : "part";
|
|
||||||
|
|
||||||
_labels = await getLabelTemplates(model_type, {
|
|
||||||
item_key: widget.part.pk.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
labels = _labels;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editPartDialog(BuildContext context) {
|
void _editPartDialog(BuildContext context) {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ void showSnackIcon(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
|
showCloseIcon: true,
|
||||||
action: onAction == null
|
action: onAction == null
|
||||||
? null
|
? null
|
||||||
: SnackBarAction(
|
: SnackBarAction(
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
|
|
||||||
final InvenTreeStockLocation? location;
|
final InvenTreeStockLocation? location;
|
||||||
|
|
||||||
List<Map<String, dynamic>> labels = [];
|
bool allowLabelPrinting = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle() {
|
String getAppBarTitle() {
|
||||||
@@ -179,19 +179,15 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.location != null && labels.isNotEmpty) {
|
if (widget.location != null &&
|
||||||
|
allowLabelPrinting &&
|
||||||
|
api.supportsModernLabelPrinting) {
|
||||||
actions.add(
|
actions.add(
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: Icon(TablerIcons.printer),
|
child: Icon(TablerIcons.printer),
|
||||||
label: L10().printLabel,
|
label: L10().printLabel,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
selectAndPrintLabel(
|
selectAndPrintLabel(context, "stocklocation", widget.location!.pk);
|
||||||
context,
|
|
||||||
labels,
|
|
||||||
widget.location!.pk,
|
|
||||||
"location",
|
|
||||||
"location=${widget.location!.pk}",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -236,33 +232,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Map<String, dynamic>> _labels = [];
|
allowLabelPrinting = await InvenTreeSettingsManager().getBool(
|
||||||
bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(
|
|
||||||
INV_ENABLE_LABEL_PRINTING,
|
INV_ENABLE_LABEL_PRINTING,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
allowLabelPrinting &= api.supportsMixin("labels");
|
|
||||||
|
|
||||||
if (allowLabelPrinting) {
|
|
||||||
if (widget.location != null) {
|
|
||||||
String model_type = api.supportsModernLabelPrinting
|
|
||||||
? InvenTreeStockLocation.MODEL_TYPE
|
|
||||||
: "location";
|
|
||||||
String item_key = api.supportsModernLabelPrinting
|
|
||||||
? "items"
|
|
||||||
: "location";
|
|
||||||
|
|
||||||
_labels = await getLabelTemplates(model_type, {
|
|
||||||
item_key: widget.location!.pk.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
labels = _labels;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _newLocation(BuildContext context) async {
|
Future<void> _newLocation(BuildContext context) async {
|
||||||
|
|||||||
@@ -128,19 +128,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (labels.isNotEmpty) {
|
if (allowLabelPrinting && api.supportsModernLabelPrinting) {
|
||||||
actions.add(
|
actions.add(
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: Icon(TablerIcons.printer),
|
child: Icon(TablerIcons.printer),
|
||||||
label: L10().printLabel,
|
label: L10().printLabel,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
selectAndPrintLabel(
|
selectAndPrintLabel(context, "stockitem", widget.item.pk);
|
||||||
context,
|
|
||||||
labels,
|
|
||||||
widget.item.pk,
|
|
||||||
"stock",
|
|
||||||
"item=${widget.item.pk}",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -196,10 +190,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is label printing enabled for this StockItem?
|
bool allowLabelPrinting = false;
|
||||||
// This will be determined when the widget is loaded
|
|
||||||
List<Map<String, dynamic>> labels = [];
|
|
||||||
|
|
||||||
int attachmentCount = 0;
|
int attachmentCount = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -318,31 +309,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Map<String, dynamic>> _labels = [];
|
allowLabelPrinting = await InvenTreeSettingsManager().getBool(
|
||||||
bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(
|
|
||||||
INV_ENABLE_LABEL_PRINTING,
|
INV_ENABLE_LABEL_PRINTING,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
allowLabelPrinting &= api.supportsMixin("labels");
|
|
||||||
|
|
||||||
// Request information on labels available for this stock item
|
|
||||||
if (allowLabelPrinting) {
|
|
||||||
String model_type = api.supportsModernLabelPrinting
|
|
||||||
? InvenTreeStockItem.MODEL_TYPE
|
|
||||||
: "stock";
|
|
||||||
String item_key = api.supportsModernLabelPrinting ? "items" : "item";
|
|
||||||
|
|
||||||
// Clear the existing labels list
|
|
||||||
_labels = await getLabelTemplates(model_type, {
|
|
||||||
item_key: widget.item.pk.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
labels = _labels;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the stock item from the database
|
/// Delete the stock item from the database
|
||||||
|
|||||||
Reference in New Issue
Block a user