2
0
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:
Oliver
2025-11-22 07:26:37 +11:00
committed by GitHub
parent e41842a31d
commit 13d95dd1b1
10 changed files with 425 additions and 368 deletions

View File

@@ -1,6 +1,7 @@
### 0.21.0 - November 2025
---
- Support label printing again, fixing issues with new printing API
- Adds zoom controller for barcode scanner camera view
- Display default stock location in Part detail page
- Display stock information in SupplierPart detail page

View File

@@ -7,6 +7,7 @@ import "package:http/http.dart" as http;
import "package:http/io_client.dart";
import "package:intl/intl.dart";
import "package:inventree/main.dart";
import "package:inventree/widget/progress.dart";
import "package:one_context/one_context.dart";
import "package:open_filex/open_filex.dart";
import "package:cached_network_image/cached_network_image.dart";
@@ -912,6 +913,8 @@ class InvenTreeAPI {
var client = createClient(url, strictHttps: strictHttps);
showLoadingOverlay();
// Attempt to open a connection to the server
try {
_request = await client
@@ -953,6 +956,7 @@ class InvenTreeAPI {
await localFile.writeAsBytes(bytes);
if (openOnDownload) {
hideLoadingOverlay();
OpenFilex.open(local_path);
}
} else {
@@ -972,6 +976,8 @@ class InvenTreeAPI {
stackTrace,
);
}
hideLoadingOverlay();
}
/*
@@ -1085,8 +1091,15 @@ class InvenTreeAPI {
* We send this with the currently selected "locale",
* so that (hopefully) the field messages are correctly translated
*/
Future<APIResponse> options(String url) async {
HttpClientRequest? request = await apiRequest(url, "OPTIONS");
Future<APIResponse> options(
String url, {
Map<String, String> params = const {},
}) async {
HttpClientRequest? request = await apiRequest(
url,
"OPTIONS",
urlParams: params,
);
if (request == null) {
// Return an "invalid" APIResponse

View File

@@ -26,23 +26,52 @@ import "package:inventree/widget/fields.dart";
import "package:inventree/widget/progress.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",
* defined by the InvenTree API
*/
class APIFormField {
// Constructor
APIFormField(this.name, this.data);
APIFormField(this.name, this.data, {this.formHandler});
// File to be uploaded for this filed
File? attachedfile;
APIFormWidgetState? formHandler;
// Name of this field
final String name;
// JSON data which defines the field
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
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
String get api_url => (getParameter("api_url") ?? "") as String;
@@ -244,18 +275,13 @@ class APIFormField {
return;
}
int? pk = int.tryParse(value.toString());
if (pk == null) {
return;
}
String url = api_url + "/" + pk.toString() + "/";
String url = api_url + "/" + value.toString() + "/";
final APIResponse response = await InvenTreeAPI().get(url, params: filters);
if (response.successful()) {
initial_data = response.data;
formHandler?.onValueChanged(name, value);
}
}
@@ -269,6 +295,7 @@ class APIFormField {
return _constructBoolean();
case "related field":
return _constructRelatedField();
case "integer":
case "float":
case "decimal":
return _constructFloatField();
@@ -318,8 +345,7 @@ class APIFormField {
onPressed: () async {
var handler = UniqueBarcodeHandler((String hash) {
controller.text = hash;
data["value"] = hash;
setFieldValue(hash);
barcodeSuccess(L10().barcodeAssigned);
});
@@ -347,9 +373,9 @@ class APIFormField {
onChanged: (DateTime? time) {
// Save the time string
if (time == null) {
data["value"] = null;
setFieldValue(null);
} else {
data["value"] = time.toString().split(" ").first;
setFieldValue(time.toString().split(" ").first);
}
},
onShowPicker: (context, value) async {
@@ -432,9 +458,9 @@ class APIFormField {
},
onSaved: (item) {
if (item == null) {
data["value"] = null;
setFieldValue(null);
} else {
data["value"] = item["value"];
setFieldValue(item["value"]);
}
},
);
@@ -481,7 +507,7 @@ class APIFormField {
return null;
},
onSaved: (val) {
data["value"] = val;
setFieldValue(val);
},
);
}
@@ -527,7 +553,20 @@ class APIFormField {
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) {
Map<String, dynamic> data = item as Map<String, dynamic>;
@@ -551,13 +590,6 @@ class APIFormField {
dropdownBuilder: (context, item) {
return _renderRelatedField(name, item, true, false);
},
onSaved: (item) {
if (item != null) {
data["value"] = item["pk"];
} else {
data["value"] = null;
}
},
compareFn: (dynamic item, dynamic selectedItem) {
// Comparison is based on the PK value
@@ -568,7 +600,8 @@ class APIFormField {
bool result = false;
try {
result = item["pk"].toString() == selectedItem["pk"].toString();
result =
item[pk_field].toString() == selectedItem[pk_field].toString();
} catch (error) {
// Catch any conversion errors
result = false;
@@ -765,6 +798,18 @@ class APIFormField {
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:
return ListTile(
title: Text(
@@ -810,8 +855,11 @@ class APIFormField {
maxLines: multiline ? null : 1,
expands: false,
initialValue: (value ?? "") as String,
onChanged: (val) {
setFieldValue(val);
},
onSaved: (val) {
data["value"] = val;
setFieldValue(val);
},
validator: (value) {
if (required && (value == null || value.isEmpty)) {
@@ -842,7 +890,7 @@ class APIFormField {
initial: initial_value,
tristate: (getParameter("tristate") ?? false) as bool,
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.
*
@@ -981,6 +1008,7 @@ Future<void> launchApiForm(
Function(Map<String, dynamic>)? onSuccess,
bool Function(Map<String, dynamic>)? validate,
Function? onCancel,
APIFormWidgetState? formHandler,
IconData icon = TablerIcons.device_floppy,
}) async {
showLoadingOverlay();
@@ -1041,7 +1069,7 @@ Future<void> launchApiForm(
field.data["instance_value"] = model_value;
if (field.data["value"] == null) {
field.data["value"] = model_value;
field.setFieldValue(model_value);
}
}
formFields.add(field);
@@ -1066,6 +1094,7 @@ Future<void> launchApiForm(
onSuccess: onSuccess,
validate: validate,
fileField: fileField,
state: formHandler,
icon: icon,
),
),
@@ -1079,6 +1108,7 @@ class APIFormWidget extends StatefulWidget {
this.fields,
this.method, {
Key? key,
this.state,
this.onSuccess,
this.validate,
this.fileField = "",
@@ -1105,12 +1135,15 @@ class APIFormWidget extends StatefulWidget {
final bool Function(Map<String, dynamic>)? validate;
final APIFormWidgetState? state;
// Default form handler is constructed if none is provided
@override
_APIFormWidgetState createState() => _APIFormWidgetState();
APIFormWidgetState createState() => state ?? APIFormWidgetState();
}
class _APIFormWidgetState extends State<APIFormWidget> {
_APIFormWidgetState() : super();
class APIFormWidgetState extends State<APIFormWidget> {
APIFormWidgetState() : super();
final _formKey = GlobalKey<FormState>();
@@ -1118,6 +1151,33 @@ class _APIFormWidgetState extends State<APIFormWidget> {
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> widgets = [];
@@ -1135,7 +1195,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
widgets.add(Divider(height: 5));
}
for (var field in widget.fields) {
for (var field in formFields) {
if (field.hidden) {
continue;
}
@@ -1190,7 +1250,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
// Pop the "file" field
data.remove(widget.fileField);
for (var field in widget.fields) {
for (var field in formFields) {
if (field.name == widget.fileField) {
File? file = field.attachedfile;
@@ -1275,7 +1335,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
match = true;
continue;
default:
for (var field in widget.fields) {
for (var field in formFields) {
// Hidden fields can't display errors, so we won't match
if (field.hidden) {
continue;
@@ -1327,7 +1387,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
// Iterate through and find "simple" top-level fields
for (var field in widget.fields) {
for (var field in formFields) {
if (field.readOnly) {
continue;
}
@@ -1366,20 +1426,11 @@ class _APIFormWidgetState extends State<APIFormWidget> {
return;
}
// Run custom onSuccess function
var successFunc = widget.onSuccess;
// An "empty" URL means we don't want to submit the form anywhere
// Perhaps we just want to process the data?
if (widget.url.isEmpty) {
// Hide the form
Navigator.pop(context);
if (successFunc != null) {
// Return the raw "submitted" data, rather than the server response
successFunc(data);
}
handleSuccess(data, {});
return;
}
@@ -1394,29 +1445,24 @@ class _APIFormWidgetState extends State<APIFormWidget> {
case 200:
case 201:
// Form was successfully validated by the server
// Ensure the response is a valid JSON structure
Map<String, dynamic> json = {};
// Hide this form
Navigator.pop(context);
var responseData = response.asMap();
if (successFunc != null) {
// Ensure the response is a valid JSON structure
Map<String, dynamic> json = {};
var data = response.asMap();
for (String key in data.keys) {
json[key.toString()] = data[key];
}
successFunc(json);
for (String key in responseData.keys) {
json[key.toString()] = responseData[key];
}
handleSuccess(data, json);
return;
case 400:
// Form submission / validation error
showSnackIcon(L10().formError, success: false);
// Update field errors
for (var field in widget.fields) {
for (var field in formFields) {
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
Widget build(BuildContext context) {
return Scaffold(
@@ -1463,18 +1525,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
),
],
),
body: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildForm(),
),
padding: EdgeInsets.all(16),
),
),
body: buildForm(context),
);
}
}

View File

@@ -695,6 +695,12 @@
"keywords": "Keywords",
"@keywords": {},
"labelDriver": "Label Driver",
"@labelDriver": {},
"labelSelectDriver": "Select Label Printer Driver",
"@labelSelectDriver": {},
"labelPrinting": "Label Printing",
"@labelPrinting": {},

View File

@@ -1,11 +1,206 @@
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/widget/progress.dart";
import "package:inventree/preferences.dart";
import "package:inventree/api_form.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/progress.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,
* and print against the selected instances.
@@ -13,202 +208,73 @@ import "package:inventree/widget/snacks.dart";
*/
Future<void> selectAndPrintLabel(
BuildContext context,
List<Map<String, dynamic>> labels,
int instanceId,
String labelType,
String labelQuery,
int instanceId,
) async {
if (!InvenTreeAPI().isConnected()) {
return;
}
// Find a list of available plugins which support label printing
var plugins = InvenTreeAPI().getPlugins(mixin: "labels");
dynamic initial_label;
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 (!InvenTreeAPI().supportsModernLabelPrinting) {
// Legacy label printing API not supported
showSnackIcon("Label printing not supported by server", success: false);
return;
}
if (label_options.length == 1) {
initial_label = label_options.first["value"];
// Fetch default values for label printing
// 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
for (var plugin in plugins) {
plugin_options.add({"display_name": plugin.humanName, "value": plugin.key});
}
String selectedPlugin = await InvenTreeAPI().getUserSetting(
"LABEL_DEFAULT_PRINTER",
// Default plugin
final defaultPlugin = await InvenTreeSettingsManager().getValue(
INV_LABEL_DEFAULT_PLUGIN,
null,
);
if (selectedPlugin.isNotEmpty) {
initial_plugin = selectedPlugin;
} else if (plugin_options.length == 1) {
initial_plugin = plugin_options.first["value"];
}
Map<String, dynamic> fields = {
"label": {
"label": L10().labelTemplate,
"type": "choice",
"value": initial_label,
"choices": label_options,
"required": true,
// Specify a default list of fields for printing
// The selected plugin may optionally extend this list of fields dynamically
Map<String, Map<String, dynamic>> baseFields = {
"template": {
"default": defaultTemplate,
"filters": {
"enabled": true,
"model_type": labelType,
"items": instanceId.toString(),
},
},
"plugin": {
"label": L10().pluginPrinter,
"type": "choice",
"value": initial_plugin,
"choices": plugin_options,
"required": true,
"default": defaultPlugin,
"pk_field": "key",
"filters": {"enabled": true, "mixin": "labels"},
},
"items": {
"hidden": true,
"value": [instanceId],
},
};
final formHandler = LabelFormWidgetState();
formHandler.labelType = labelType;
launchApiForm(
context,
L10().printLabel,
"",
fields,
icon: TablerIcons.printer,
validate: (Map<String, dynamic> data) {
final template = data["label"];
final plugin = data["plugin"];
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);
}
}
PRINT_LABEL_URL,
baseFields,
method: "POST",
modelData: {"plugin": defaultPlugin, "template": defaultTemplate},
formHandler: formHandler,
onSuccess: (data) async {
handlePrintingSuccess(context, data, 0);
},
);
}
/*
* 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;
}

View File

@@ -25,7 +25,11 @@ const int SCREEN_ORIENTATION_LANDSCAPE = 2;
const String INV_SOUNDS_BARCODE = "barcodeSounds";
const String INV_SOUNDS_SERVER = "serverSounds";
// Label printing settings
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
const String INV_PART_SHOW_PARAMETERS = "partShowParameters";

View File

@@ -63,8 +63,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
InvenTreePartPricing? partPricing;
List<Map<String, dynamic>> labels = [];
@override
String getAppBarTitle() => L10().partDetails;
@@ -121,19 +119,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
);
}
if (labels.isNotEmpty) {
if (allowLabelPrinting && api.supportsModernLabelPrinting) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.printer),
label: L10().printLabel,
onTap: () async {
selectAndPrintLabel(
context,
labels,
widget.part.pk,
"part",
"part=${widget.part.pk}",
);
selectAndPrintLabel(context, "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) {

View File

@@ -64,6 +64,7 @@ void showSnackIcon(
},
),
backgroundColor: backgroundColor,
showCloseIcon: true,
action: onAction == null
? null
: SnackBarAction(

View File

@@ -38,7 +38,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
final InvenTreeStockLocation? location;
List<Map<String, dynamic>> labels = [];
bool allowLabelPrinting = false;
@override
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(
SpeedDialChild(
child: Icon(TablerIcons.printer),
label: L10().printLabel,
onTap: () async {
selectAndPrintLabel(
context,
labels,
widget.location!.pk,
"location",
"location=${widget.location!.pk}",
);
selectAndPrintLabel(context, "stocklocation", widget.location!.pk);
},
),
);
@@ -236,33 +232,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
}
}
List<Map<String, dynamic>> _labels = [];
bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(
allowLabelPrinting = await InvenTreeSettingsManager().getBool(
INV_ENABLE_LABEL_PRINTING,
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 {

View File

@@ -128,19 +128,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
);
}
if (labels.isNotEmpty) {
if (allowLabelPrinting && api.supportsModernLabelPrinting) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.printer),
label: L10().printLabel,
onTap: () async {
selectAndPrintLabel(
context,
labels,
widget.item.pk,
"stock",
"item=${widget.item.pk}",
);
selectAndPrintLabel(context, "stockitem", widget.item.pk);
},
),
);
@@ -196,10 +190,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
return actions;
}
// Is label printing enabled for this StockItem?
// This will be determined when the widget is loaded
List<Map<String, dynamic>> labels = [];
bool allowLabelPrinting = false;
int attachmentCount = 0;
@override
@@ -318,31 +309,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
}
List<Map<String, dynamic>> _labels = [];
bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(
allowLabelPrinting = await InvenTreeSettingsManager().getBool(
INV_ENABLE_LABEL_PRINTING,
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