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,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;
}