From 443e6e856c8d8c2696c9d0d3102afb2f4bcc1372 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 16 Jul 2023 00:51:11 +1000 Subject: [PATCH] Label print updates (#399) * Allow download of printed label * Add setting for controlling label printing * Control display of label printing via setting * Refactor label printing functionality - Move to helpers.dart - Will be used for other label types also * Factor out request for label templates * Add label printing support for part * Support label printing for stock location * update release notes --- assets/release_notes.md | 6 ++ lib/helpers.dart | 4 +- lib/l10n/app_en.arb | 6 ++ lib/labels.dart | 155 +++++++++++++++++++++++++++++++ lib/preferences.dart | 2 + lib/settings/app_settings.dart | 23 ++++- lib/settings/settings.dart | 2 +- lib/widget/location_display.dart | 36 +++++++ lib/widget/part_detail.dart | 43 +++++++-- lib/widget/stock_detail.dart | 141 +++++----------------------- 10 files changed, 284 insertions(+), 134 deletions(-) create mode 100644 lib/labels.dart diff --git a/assets/release_notes.md b/assets/release_notes.md index 9babb132..ddf90a1c 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,3 +1,9 @@ +### 0.12.6 - July 2023 +--- + +- Enable label printing for stock locations +- Enable label printing for parts + ### 0.12.5 - July 2023 --- diff --git a/lib/helpers.dart b/lib/helpers.dart index f958e6af..49a880d1 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -9,6 +9,7 @@ import "dart:io"; import "package:currency_formatter/currency_formatter.dart"; + import "package:one_context/one_context.dart"; import "package:url_launcher/url_launcher.dart"; import "package:audioplayers/audioplayers.dart"; @@ -132,4 +133,5 @@ String renderCurrency(double? amount, String currency, {int decimals = 2}) { } return value; -} \ No newline at end of file +} + diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3bef7360..9589f9ba 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -533,6 +533,12 @@ "keywords": "Keywords", "@keywords": {}, + "labelPrinting": "Label Printing", + "@labelPrinting": {}, + + "labelPrintingDetail": "Enable label printing", + "@labelPrintingDetail": {}, + "labelTemplate": "Label Template", "@labelTemplate": {}, diff --git a/lib/labels.dart b/lib/labels.dart new file mode 100644 index 00000000..ca4f8ad2 --- /dev/null +++ b/lib/labels.dart @@ -0,0 +1,155 @@ +import "package:flutter/cupertino.dart"; +import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:inventree/api.dart"; +import "package:inventree/widget/progress.dart"; +import "package:inventree/api_form.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/widget/snacks.dart"; + +/* + * 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"; + + List> labels = []; + + await InvenTreeAPI().get( + "/label/${labelType}/", + 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; +} + + +/* + * Select a particular label, from a provided list of options, + * and print against the selected instances. + */ +Future selectAndPrintLabel( + BuildContext context, + List> labels, + String labelType, + String labelQuery, + ) 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 display_name = (label["description"] ?? "").toString(); + int pk = (label["pk"] ?? -1) as int; + + if (display_name.isNotEmpty && pk > 0) { + label_options.add({ + "display_name": display_name, + "value": pk, + }); + } + } + + if (label_options.length == 1) { + initial_label = label_options.first["value"]; + } + + // Construct list of available plugins + for (var plugin in plugins) { + plugin_options.add({ + "display_name": plugin.humanName, + "value": plugin.key + }); + } + + 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, + }, + "plugin": { + "label": L10().pluginPrinter, + "type": "choice", + "value": initial_plugin, + "choices": plugin_options, + "required": true, + } + }; + + launchApiForm( + context, + L10().printLabel, + "", + fields, + icon: FontAwesomeIcons.print, + onSuccess: (Map data) async { + int labelId = (data["label"] ?? -1) as int; + String pluginKey = (data["plugin"] ?? "") as String; + + if (labelId != -1 && pluginKey.isNotEmpty) { + String url = "/label/${labelType}/${labelId}/print/?${labelQuery}&plugin=${pluginKey}"; + + showLoadingOverlay(context); + + InvenTreeAPI().get(url).then((APIResponse response) { + hideLoadingOverlay(); + 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); + } else { + showSnackIcon( + L10().printLabelSuccess, + success: true + ); + } + } else { + showSnackIcon( + L10().printLabelFailure, + success: false, + ); + } + }); + } + }, + ); +} \ No newline at end of file diff --git a/lib/preferences.dart b/lib/preferences.dart index a7e8a30a..51801527 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -25,6 +25,8 @@ const int SCREEN_ORIENTATION_LANDSCAPE = 2; const String INV_SOUNDS_BARCODE = "barcodeSounds"; const String INV_SOUNDS_SERVER = "serverSounds"; +const String INV_ENABLE_LABEL_PRINTING = "enableLabelPrinting"; + // Part settings const String INV_PART_SHOW_PARAMETERS = "partShowParameters"; const String INV_PART_SHOW_BOM = "partShowBom"; diff --git a/lib/settings/app_settings.dart b/lib/settings/app_settings.dart index 00c27a7d..1937ff54 100644 --- a/lib/settings/app_settings.dart +++ b/lib/settings/app_settings.dart @@ -1,18 +1,18 @@ import "package:flutter/material.dart"; +import "package:one_context/one_context.dart"; import "package:adaptive_theme/adaptive_theme.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:flutter_localized_locales/flutter_localized_locales.dart"; -import "package:inventree/app_colors.dart"; -import "package:inventree/widget/dialogs.dart"; -import "package:one_context/one_context.dart"; +import "package:inventree/app_colors.dart"; import "package:inventree/api_form.dart"; import "package:inventree/l10.dart"; import "package:inventree/l10n/supported_locales.dart"; import "package:inventree/main.dart"; import "package:inventree/preferences.dart"; +import "package:inventree/widget/dialogs.dart"; import "package:inventree/widget/progress.dart"; @@ -33,7 +33,7 @@ class _InvenTreeAppSettingsState extends State { bool reportErrors = true; bool strictHttps = false; - + bool enableLabelPrinting = true; bool darkMode = false; int screenOrientation = SCREEN_ORIENTATION_SYSTEM; @@ -56,6 +56,7 @@ class _InvenTreeAppSettingsState extends State { reportErrors = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool; strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool; screenOrientation = await InvenTreeSettingsManager().getValue(INV_SCREEN_ORIENTATION, SCREEN_ORIENTATION_SYSTEM) as int; + enableLabelPrinting = await InvenTreeSettingsManager().getValue(INV_ENABLE_LABEL_PRINTING, true) as bool; darkMode = AdaptiveTheme.of(context).mode.isDark; @@ -218,6 +219,20 @@ class _InvenTreeAppSettingsState extends State { ); }, ), + ListTile( + title: Text(L10().labelPrinting), + subtitle: Text(L10().labelPrintingDetail), + leading: FaIcon(FontAwesomeIcons.print), + trailing: Switch( + value: enableLabelPrinting, + onChanged: (bool value) { + InvenTreeSettingsManager().setValue(INV_ENABLE_LABEL_PRINTING, value); + setState(() { + enableLabelPrinting = value; + }); + } + ), + ), ListTile( title: Text(L10().strictHttps), subtitle: Text(L10().strictHttpsDetails), diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index f5b8e2d5..8c420718 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -13,8 +13,8 @@ import "package:inventree/settings/login.dart"; import "package:inventree/settings/part_settings.dart"; +// InvenTree settings view class InvenTreeSettingsWidget extends StatefulWidget { - // InvenTree settings view @override _InvenTreeSettingsState createState() => _InvenTreeSettingsState(); diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index f1da729e..a26ddc98 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -8,6 +8,7 @@ import "package:inventree/barcode/barcode.dart"; import "package:inventree/l10.dart"; import "package:inventree/inventree/stock.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/widget/location_list.dart"; import "package:inventree/widget/progress.dart"; @@ -15,6 +16,7 @@ import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/stock_detail.dart"; import "package:inventree/widget/stock_list.dart"; +import "package:inventree/labels.dart"; /* @@ -38,6 +40,10 @@ class _LocationDisplayState extends RefreshableState { final InvenTreeStockLocation? location; + bool allowLabelPrinting = true; + + List> labels = []; + @override String getAppBarTitle() { return L10().stockLocation; @@ -163,6 +169,23 @@ class _LocationDisplayState extends RefreshableState { ); } + if (widget.location != null && allowLabelPrinting && labels.isNotEmpty) { + actions.add( + SpeedDialChild( + child: FaIcon(FontAwesomeIcons.print), + label: L10().printLabel, + onTap: () async { + selectAndPrintLabel( + context, + labels, + "location", + "location=${widget.location!.pk}" + ); + } + ) + ); + } + return actions; } @@ -202,6 +225,19 @@ class _LocationDisplayState extends RefreshableState { } } + allowLabelPrinting = await InvenTreeSettingsManager().getBool(INV_ENABLE_LABEL_PRINTING, true); + allowLabelPrinting &= api.getPlugins(mixin: "labels").isNotEmpty; + + if (allowLabelPrinting) { + labels.clear(); + + if (widget.location != null) { + labels = await getLabelTemplates("location", { + "location": widget.location!.pk.toString() + }); + } + } + if (mounted) { setState(() {}); } diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 4943410c..d8a81b9c 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -11,6 +11,7 @@ import "package:inventree/helpers.dart"; import "package:inventree/inventree/bom.dart"; import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/stock.dart"; +import "package:inventree/labels.dart"; import "package:inventree/preferences.dart"; import "package:inventree/widget/attachment_widget.dart"; @@ -54,17 +55,16 @@ class _PartDisplayState extends RefreshableState { int parameterCount = 0; bool showParameters = false; - bool showBom = false; + bool allowLabelPrinting = true; int attachmentCount = 0; - int bomCount = 0; - int usedInCount = 0; - int variantCount = 0; + List> labels = []; + @override String getAppBarTitle() => L10().partDetails; @@ -110,12 +110,29 @@ class _PartDisplayState extends RefreshableState { List actions = []; if (InvenTreeStockItem().canCreate) { + actions.add( + SpeedDialChild( + child: FaIcon(FontAwesomeIcons.box), + label: L10().stockItemCreate, + onTap: () { + _newStockItem(context); + } + ) + ); + } + + if (allowLabelPrinting && labels.isNotEmpty) { actions.add( SpeedDialChild( - child: FaIcon(FontAwesomeIcons.box), - label: L10().stockItemCreate, - onTap: () { - _newStockItem(context); + child: FaIcon(FontAwesomeIcons.print), + label: L10().printLabel, + onTap: () async { + selectAndPrintLabel( + context, + labels, + "part", + "part=${widget.part.pk}" + ); } ) ); @@ -226,6 +243,16 @@ class _PartDisplayState extends RefreshableState { }); } }); + + allowLabelPrinting = await InvenTreeSettingsManager().getBool(INV_ENABLE_LABEL_PRINTING, true); + allowLabelPrinting &= api.getPlugins(mixin: "labels").isNotEmpty; + + if (allowLabelPrinting) { + labels.clear(); + labels = await getLabelTemplates("part", { + "part": widget.part.pk.toString(), + }); + } } void _editPartDialog(BuildContext context) { diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index c33c9578..2b6f983f 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -9,6 +9,7 @@ import "package:inventree/helpers.dart"; import "package:inventree/l10.dart"; import "package:inventree/api.dart"; import "package:inventree/api_form.dart"; +import "package:inventree/labels.dart"; import "package:inventree/preferences.dart"; import "package:inventree/inventree/company.dart"; @@ -127,13 +128,18 @@ class _StockItemDisplayState extends RefreshableState { ); } - if (labels.isNotEmpty) { + if (allowLabelPrinting && labels.isNotEmpty) { actions.add( SpeedDialChild( child: FaIcon(FontAwesomeIcons.print), label: L10().printLabel, - onTap: () { - _printLabel(context); + onTap: () async { + selectAndPrintLabel( + context, + labels, + "stock", + "item=${widget.item.pk}" + ); } ) ); @@ -198,9 +204,10 @@ class _StockItemDisplayState extends RefreshableState { int attachmentCount = 0; + bool allowLabelPrinting = true; + @override Future onBuild(BuildContext context) async { - // Load part data if not already loaded if (part == null) { refresh(context); @@ -209,9 +216,7 @@ class _StockItemDisplayState extends RefreshableState { @override Future request(BuildContext context) async { - await api.StockStatus.load(); - stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool; stockShowTests = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_TESTS, true) as bool; @@ -254,43 +259,20 @@ class _StockItemDisplayState extends RefreshableState { } }); + // Determine if label printing is supported + allowLabelPrinting = await InvenTreeSettingsManager().getBool(INV_ENABLE_LABEL_PRINTING, true); + allowLabelPrinting &= api.getPlugins(mixin: "labels").isNotEmpty; + // Request information on labels available for this stock item - if (InvenTreeAPI().pluginsEnabled()) { - _getLabels(); + if (allowLabelPrinting) { + // Clear the existing labels list + labels.clear(); + labels = await getLabelTemplates("stock", { + "item": widget.item.pk.toString() + }); } } - Future _getLabels() async { - // Clear the existing labels list - labels.clear(); - - // If the server does not support label printing, don't bother! - if (!InvenTreeAPI().supportsMixin("labels")) { - return; - } - - InvenTreeAPI().get( - "/label/stock/", - params: { - "enabled": "true", - "item": "${widget.item.pk}", - }, - ).then((APIResponse response) { - if (response.isValid() && response.statusCode == 200) { - - for (var label in response.resultsList()) { - if (label is Map) { - labels.add(label); - } - } - - if (mounted) { - setState(() {}); - } - } - }); - } - /// Delete the stock item from the database Future _deleteItem(BuildContext context) async { @@ -314,87 +296,6 @@ class _StockItemDisplayState extends RefreshableState { } - /// Opens a popup dialog allowing user to select a label for printing - Future _printLabel(BuildContext context) async { - - var plugins = InvenTreeAPI().getPlugins(mixin: "labels"); - - dynamic initial_label; - dynamic initial_plugin; - - List> label_options = []; - List> plugin_options = []; - - for (var label in labels) { - label_options.add({ - "display_name": label["description"], - "value": label["pk"], - }); - } - - for (var plugin in plugins) { - plugin_options.add({ - "display_name": plugin.humanName, - "value": plugin.key, - }); - } - - if (labels.length == 1) { - initial_label = labels.first["pk"]; - } - - if (plugins.length == 1) { - initial_plugin = plugins.first.key; - } - - Map fields = { - "label": { - "label": L10().labelTemplate, - "type": "choice", - "value": initial_label, - "choices": label_options, - "required": true, - }, - "plugin": { - "label": L10().pluginPrinter, - "type": "choice", - "value": initial_plugin, - "choices": plugin_options, - "required": true, - } - }; - - launchApiForm( - context, - L10().printLabel, - "", - fields, - icon: FontAwesomeIcons.print, - onSuccess: (Map data) async { - int labelId = (data["label"] ?? -1) as int; - String pluginKey = (data["plugin"] ?? "") as String; - - if (labelId != -1 && pluginKey.isNotEmpty) { - String url = "/label/stock/${labelId}/print/?item=${widget.item.pk}&plugin=${pluginKey}"; - - InvenTreeAPI().get(url).then((APIResponse response) { - if (response.isValid() && response.statusCode == 200) { - showSnackIcon( - L10().printLabelSuccess, - success: true - ); - } else { - showSnackIcon( - L10().printLabelFailure, - success: false, - ); - } - }); - } - }, - ); - } - Future _editStockItem(BuildContext context) async { var fields = InvenTreeStockItem().formFields();