From 13d95dd1b17328748b62e7f4eb49871ac1137d55 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 22 Nov 2025 07:26:37 +1100 Subject: [PATCH] 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 --- assets/release_notes.md | 1 + lib/api.dart | 17 +- lib/api_form.dart | 233 ++++++++------ lib/l10n/app_en.arb | 6 + lib/labels.dart | 422 ++++++++++++++----------- lib/preferences.dart | 4 + lib/widget/part/part_detail.dart | 32 +- lib/widget/snacks.dart | 1 + lib/widget/stock/location_display.dart | 39 +-- lib/widget/stock/stock_detail.dart | 38 +-- 10 files changed, 425 insertions(+), 368 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index 21b0d09a..f97832ac 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -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 diff --git a/lib/api.dart b/lib/api.dart index a2a48f9d..1d24b397 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -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 options(String url) async { - HttpClientRequest? request = await apiRequest(url, "OPTIONS"); + Future options( + String url, { + Map params = const {}, + }) async { + HttpClientRequest? request = await apiRequest( + url, + "OPTIONS", + urlParams: params, + ); if (request == null) { // Return an "invalid" APIResponse diff --git a/lib/api_form.dart b/lib/api_form.dart index 7852c452..ea0bc79b 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -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 extractFields(APIResponse response) { + if (!response.isValid()) { + return {}; + } + + var data = response.asMap(); + + if (!data.containsKey("actions")) { + return {}; + } + + var actions = response.data["actions"] as Map; + + dynamic result = actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {}; + + return result as Map; +} + /* * 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 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 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 data = item as Map; @@ -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 extractFields(APIResponse response) { - if (!response.isValid()) { - return {}; - } - - var data = response.asMap(); - - if (!data.containsKey("actions")) { - return {}; - } - - var actions = response.data["actions"] as Map; - - dynamic result = actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {}; - - return result as Map; -} - /* * Extract a field definition (map) from the provided JSON data. * @@ -981,6 +1008,7 @@ Future launchApiForm( Function(Map)? onSuccess, bool Function(Map)? validate, Function? onCancel, + APIFormWidgetState? formHandler, IconData icon = TablerIcons.device_floppy, }) async { showLoadingOverlay(); @@ -1041,7 +1069,7 @@ Future 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 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)? 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 { - _APIFormWidgetState() : super(); +class APIFormWidgetState extends State { + APIFormWidgetState() : super(); final _formKey = GlobalKey(); @@ -1118,6 +1151,33 @@ class _APIFormWidgetState extends State { 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 get formFields { + final List 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 handleSuccess( + Map submittedData, + Map responseData, + ) async { + widget.onSuccess?.call(responseData); + Navigator.pop(context); + } + List _buildForm() { List widgets = []; @@ -1135,7 +1195,7 @@ class _APIFormWidgetState extends State { 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 { // 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 { 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 { // 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 { 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 { case 200: case 201: // Form was successfully validated by the server + // Ensure the response is a valid JSON structure + Map json = {}; - // Hide this form - Navigator.pop(context); + var responseData = response.asMap(); - if (successFunc != null) { - // Ensure the response is a valid JSON structure - Map 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 { }); } + // 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 { ), ], ), - 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), ); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b57df1f1..9ea177e9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -695,6 +695,12 @@ "keywords": "Keywords", "@keywords": {}, + "labelDriver": "Label Driver", + "@labelDriver": {}, + + "labelSelectDriver": "Select Label Printer Driver", + "@labelSelectDriver": {}, + "labelPrinting": "Label Printing", "@labelPrinting": {}, diff --git a/lib/labels.dart b/lib/labels.dart index 10e2c205..2b7cc270 100644 --- a/lib/labels.dart +++ b/lib/labels.dart @@ -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 dynamicFields = []; + + String pluginKey = ""; + String labelType = ""; + + @override + List 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 handleSuccess( + Map submittedData, + Map 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?; + + InvenTreeSettingsManager().setValue(INV_LABEL_DEFAULT_TEMPLATES, { + ...?defaultTemplates, + labelType: template, + }); + } + } + + /* + * Re-fetch printing options when the plugin changes + */ + Future 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 updateFields(APIResponse response) async { + Map printingFields = extractFields(response); + + // Find only the fields which are not in the "base" fields + List uniqueFields = []; + + for (String key in printingFields.keys) { + if (super.formFields.any((field) => field.name == key)) { + continue; + } + + dynamic data = printingFields[key]; + + Map fieldData = {}; + + if (data is Map) { + fieldData = Map.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 child_map = child as Map; + dynamic nested_children = child_map["children"]; + + if (nested_children != null && nested_children is Map) { + Map nested_child_map = + nested_children as Map; + + 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 handlePrintingSuccess( + BuildContext context, + Map 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) { + final responseData = response.data as Map; + 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 selectAndPrintLabel( BuildContext context, - List> 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> label_options = []; - List> 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) { + 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 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> 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 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 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>> getLabelTemplates( - String labelType, - Map 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> 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) { - labels.add(label); - } - } - } - }); - - return labels; -} diff --git a/lib/preferences.dart b/lib/preferences.dart index bf0d8ae4..de7bb0f0 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -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"; diff --git a/lib/widget/part/part_detail.dart b/lib/widget/part/part_detail.dart index e54bb753..98cf1737 100644 --- a/lib/widget/part/part_detail.dart +++ b/lib/widget/part/part_detail.dart @@ -63,8 +63,6 @@ class _PartDisplayState extends RefreshableState { InvenTreePartPricing? partPricing; - List> labels = []; - @override String getAppBarTitle() => L10().partDetails; @@ -121,19 +119,13 @@ class _PartDisplayState extends RefreshableState { ); } - 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 { }); } }); - - List> _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) { diff --git a/lib/widget/snacks.dart b/lib/widget/snacks.dart index 580955f2..f00b1ec1 100644 --- a/lib/widget/snacks.dart +++ b/lib/widget/snacks.dart @@ -64,6 +64,7 @@ void showSnackIcon( }, ), backgroundColor: backgroundColor, + showCloseIcon: true, action: onAction == null ? null : SnackBarAction( diff --git a/lib/widget/stock/location_display.dart b/lib/widget/stock/location_display.dart index 7719724a..f6f74db9 100644 --- a/lib/widget/stock/location_display.dart +++ b/lib/widget/stock/location_display.dart @@ -38,7 +38,7 @@ class _LocationDisplayState extends RefreshableState { final InvenTreeStockLocation? location; - List> labels = []; + bool allowLabelPrinting = false; @override String getAppBarTitle() { @@ -179,19 +179,15 @@ class _LocationDisplayState extends RefreshableState { ); } - 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 { } } - List> _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 _newLocation(BuildContext context) async { diff --git a/lib/widget/stock/stock_detail.dart b/lib/widget/stock/stock_detail.dart index 648610c0..db295cac 100644 --- a/lib/widget/stock/stock_detail.dart +++ b/lib/widget/stock/stock_detail.dart @@ -128,19 +128,13 @@ class _StockItemDisplayState extends RefreshableState { ); } - 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 { return actions; } - // Is label printing enabled for this StockItem? - // This will be determined when the widget is loaded - List> labels = []; - + bool allowLabelPrinting = false; int attachmentCount = 0; @override @@ -318,31 +309,10 @@ class _StockItemDisplayState extends RefreshableState { } } - List> _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