From 2e798b1bd1143dade02f9db5a6062af0030110de Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 5 Dec 2024 14:38:53 +1100 Subject: [PATCH] Order picture action (#557) * Add "take picture" to purchase order detail * Rename uploaded images * Provide prefix when uploading images * Add similar functionality for "sales order" detail * Add new settings screens * Control camera shortcut * Bump release notes --- assets/release_notes.md | 3 + lib/inventree/model.dart | 46 ++++++++++-- lib/l10n/app_en.arb | 30 ++++++++ lib/preferences.dart | 8 +++ lib/settings/part_settings.dart | 10 +-- lib/settings/purchase_order_settings.dart | 79 +++++++++++++++++++++ lib/settings/sales_order_settings.dart | 79 +++++++++++++++++++++ lib/settings/settings.dart | 19 ++++- lib/widget/attachment_widget.dart | 8 ++- lib/widget/company/company_detail.dart | 1 + lib/widget/order/purchase_order_detail.dart | 25 +++++++ lib/widget/order/sales_order_detail.dart | 24 +++++++ lib/widget/part/part_detail.dart | 5 +- lib/widget/stock/stock_detail.dart | 1 + 14 files changed, 321 insertions(+), 17 deletions(-) create mode 100644 lib/settings/purchase_order_settings.dart create mode 100644 lib/settings/sales_order_settings.dart diff --git a/assets/release_notes.md b/assets/release_notes.md index 09e86ee4..1841054c 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,6 +1,9 @@ ### 0.17.0 - November 2024 --- +- Improvements for image uploading +- Provide "upload image" shortcut on Purchase Order detail view +- Provide "upload image" shortcut on Sales Order detail view - Clearly indicate if a StockItem is unavailable ### 0.16.5 - September 2024 diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 2ee62563..852454d1 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -12,7 +12,9 @@ import "package:inventree/api_form.dart"; import "package:inventree/l10.dart"; import "package:inventree/helpers.dart"; import "package:inventree/inventree/sentry.dart"; + import "package:inventree/widget/dialogs.dart"; +import "package:inventree/widget/fields.dart"; // Paginated response object @@ -993,7 +995,7 @@ class InvenTreeAttachment extends InvenTreeModel { return count(filters: filters); } - Future uploadAttachment(File attachment, String modelType, int modelId, {String comment = "", Map fields = const {}}) async { + Future uploadAttachment(File attachment, int modelId, {String comment = "", Map fields = const {}}) async { // Ensure that the correct reference field is set Map data = Map.from(fields); @@ -1002,14 +1004,9 @@ class InvenTreeAttachment extends InvenTreeModel { if (InvenTreeAPI().supportsModernAttachments) { - if (modelType.isEmpty) { - sentryReportMessage("uploadAttachment called with empty 'modelType'"); - return false; - } - url = "attachment/"; data["model_id"] = modelId.toString(); - data["model_type"] = modelType; + data["model_type"] = REF_MODEL_TYPE; } else { @@ -1032,6 +1029,41 @@ class InvenTreeAttachment extends InvenTreeModel { return response.successful(); } + + Future uploadImage(int modelId, {String prefix = "InvenTree"}) async { + + bool result = false; + + await FilePickerDialog.pickImageFromCamera().then((File? file) { + if (file != null) { + + String dir = path.dirname(file.path); + String ext = path.extension(file.path); + String now = DateTime.now().toIso8601String().replaceAll(":", "-"); + + // Rename the file with a unique name + String filename = "${dir}/${prefix}_image_${now}${ext}"; + + try { + file.rename(filename).then((File renamed) { + uploadAttachment(renamed, modelId).then((success) { + result = success; + showSnackIcon( + result ? L10().imageUploadSuccess : L10().imageUploadFailure, + success: result); + }); + }); + } catch (error, stackTrace) { + sentryReportError("uploadImage", error, stackTrace); + showSnackIcon(L10().imageUploadFailure, success: false); + } + } + }); + + return result; + } + + /* * Download this attachment file */ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cbe1f14e..3db78e6c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -897,6 +897,18 @@ "projectCode": "Project Code", "@projectCode": {}, + "purchaseOrderEnable": "Enable Purchase Orders", + "@purchaseOrderEnable": {}, + + "purchaseOrderEnableDetail": "Enable purchase order functionality", + "@purchaseOrderEnableDetail": {}, + + "purchaseOrderShowCamera": "Camera Shortcut", + "@purchaseOrderShowCamera": {}, + + "purchaseOrderShowCameraDetail": "Enable image upload shortcut on purchase order screen", + "@purchaseOrderShowCameraDetail": {}, + "purchaseOrder": "Purchase Order", "@purchaseOrder": {}, @@ -906,6 +918,9 @@ "purchaseOrderEdit": "Edit Purchase Order", "@purchaseOrderEdit": {}, + "purchaseOrderSettings": "Purchase order settings", + "@purchaseOrderSettings": {}, + "purchaseOrders": "Purchase Orders", "@purchaseOrders": {}, @@ -1060,6 +1075,21 @@ "salesOrders": "Sales Orders", "@salesOrders": {}, + "salesOrderEnable": "Enable Sales Orders", + "@salesOrderEnable": {}, + + "salesOrderEnableDetail": "Enable sales order functionality", + "@salesOrderEnableDetail": {}, + + "salesOrderShowCamera": "Camera Shortcut", + "@salesOrderShowCamera": {}, + + "salesOrderShowCameraDetail": "Enable image upload shortcut on sales order screen", + "@salesOrderShowCameraDetail": {}, + + "salesOrderSettings": "Sales order settings", + "@salesOrderSettings": {}, + "salesOrderCreate": "New Sales Order", "@saleOrderCreate": {}, diff --git a/lib/preferences.dart b/lib/preferences.dart index 2611418f..57004873 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -36,6 +36,14 @@ const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; const String INV_STOCK_SHOW_TESTS = "stockShowTests"; const String INV_STOCK_CONFIRM_SCAN = "stockConfirmScan"; +// Purchase order settings +const String INV_PO_ENABLE = "poEnable"; +const String INV_PO_SHOW_CAMERA = "poShowCamera"; + +// Sales order settings +const String INV_SO_ENABLE = "soEnable"; +const String INV_SO_SHOW_CAMERA = "soShowCamera"; + const String INV_REPORT_ERRORS = "reportErrors"; const String INV_STRICT_HTTPS = "strictHttps"; diff --git a/lib/settings/part_settings.dart b/lib/settings/part_settings.dart index bae70955..11cba4b4 100644 --- a/lib/settings/part_settings.dart +++ b/lib/settings/part_settings.dart @@ -30,11 +30,11 @@ class _InvenTreePartSettingsState extends State { } Future loadSettings() async { - partShowParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; - partShowBom = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_BOM, true) as bool; - stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool; - stockShowTests = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_TESTS, true) as bool; - stockConfirmScan = await InvenTreeSettingsManager().getValue(INV_STOCK_CONFIRM_SCAN, false) as bool; + partShowParameters = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_PARAMETERS, true); + partShowBom = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_BOM, true); + stockShowHistory = await InvenTreeSettingsManager().getBool(INV_STOCK_SHOW_HISTORY, false); + stockShowTests = await InvenTreeSettingsManager().getBool(INV_STOCK_SHOW_TESTS, true); + stockConfirmScan = await InvenTreeSettingsManager().getBool(INV_STOCK_CONFIRM_SCAN, false); if (mounted) { setState(() { diff --git a/lib/settings/purchase_order_settings.dart b/lib/settings/purchase_order_settings.dart new file mode 100644 index 00000000..d4211c29 --- /dev/null +++ b/lib/settings/purchase_order_settings.dart @@ -0,0 +1,79 @@ + +import "package:flutter/material.dart"; +import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; + +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; + + +class InvenTreePurchaseOrderSettingsWidget extends StatefulWidget { + @override + _InvenTreePurchaseOrderSettingsState createState() => _InvenTreePurchaseOrderSettingsState(); +} + + +class _InvenTreePurchaseOrderSettingsState extends State { + + _InvenTreePurchaseOrderSettingsState(); + + bool poEnable = true; + bool poShowCamera = true; + + @override + void initState() { + super.initState(); + + loadSettings(); + } + + Future loadSettings() async { + poEnable = await InvenTreeSettingsManager().getBool(INV_PO_ENABLE, true); + poShowCamera = await InvenTreeSettingsManager().getBool(INV_PO_SHOW_CAMERA, true); + + if (mounted) { + setState(() { + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(L10().purchaseOrderSettings)), + body: Container( + child: ListView( + children: [ + ListTile( + title: Text(L10().purchaseOrderEnable), + subtitle: Text(L10().purchaseOrderEnableDetail), + leading: Icon(TablerIcons.shopping_cart), + trailing: Switch( + value: poEnable, + onChanged: (bool value) { + InvenTreeSettingsManager().setValue(INV_PO_ENABLE, value); + setState(() { + poEnable = value; + }); + }, + ), + ), + ListTile( + title: Text(L10().purchaseOrderShowCamera), + subtitle: Text(L10().purchaseOrderShowCameraDetail), + leading: Icon(TablerIcons.camera), + trailing: Switch( + value: poShowCamera, + onChanged: (bool value) { + InvenTreeSettingsManager().setValue(INV_PO_SHOW_CAMERA, value); + setState(() { + poShowCamera = value; + }); + }, + ), + ), + ] + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/settings/sales_order_settings.dart b/lib/settings/sales_order_settings.dart new file mode 100644 index 00000000..62219b96 --- /dev/null +++ b/lib/settings/sales_order_settings.dart @@ -0,0 +1,79 @@ + +import "package:flutter/material.dart"; +import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; + +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; + + +class InvenTreeSalesOrderSettingsWidget extends StatefulWidget { + @override + _InvenTreeSalesOrderSettingsState createState() => _InvenTreeSalesOrderSettingsState(); +} + + +class _InvenTreeSalesOrderSettingsState extends State { + + _InvenTreeSalesOrderSettingsState(); + + bool soEnable = true; + bool soShowCamera = true; + + @override + void initState() { + super.initState(); + + loadSettings(); + } + + Future loadSettings() async { + soEnable = await InvenTreeSettingsManager().getBool(INV_SO_ENABLE, true); + soShowCamera = await InvenTreeSettingsManager().getBool(INV_SO_SHOW_CAMERA, true); + + if (mounted) { + setState(() { + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(L10().salesOrderSettings)), + body: Container( + child: ListView( + children: [ + ListTile( + title: Text(L10().salesOrderEnable), + subtitle: Text(L10().salesOrderEnableDetail), + leading: Icon(TablerIcons.shopping_cart), + trailing: Switch( + value: soEnable, + onChanged: (bool value) { + InvenTreeSettingsManager().setValue(INV_SO_ENABLE, value); + setState(() { + soEnable = value; + }); + }, + ), + ), + ListTile( + title: Text(L10().salesOrderShowCamera), + subtitle: Text(L10().salesOrderShowCameraDetail), + leading: Icon(TablerIcons.camera), + trailing: Switch( + value: soShowCamera, + onChanged: (bool value) { + InvenTreeSettingsManager().setValue(INV_SO_SHOW_CAMERA, value); + setState(() { + soShowCamera = value; + }); + }, + ), + ), + ] + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 595a15b8..82267cef 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -11,7 +11,8 @@ import "package:inventree/settings/barcode_settings.dart"; import "package:inventree/settings/home_settings.dart"; import "package:inventree/settings/select_server.dart"; import "package:inventree/settings/part_settings.dart"; - +import "package:inventree/settings/purchase_order_settings.dart"; +import "package:inventree/settings/sales_order_settings.dart"; // InvenTree settings view class InvenTreeSettingsWidget extends StatefulWidget { @@ -86,6 +87,22 @@ class _InvenTreeSettingsState extends State { Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePartSettingsWidget())); } ), + ListTile( + title: Text(L10().purchaseOrder), + subtitle: Text(L10().purchaseOrderSettings), + leading: Icon(TablerIcons.shopping_cart, color: COLOR_ACTION), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePurchaseOrderSettingsWidget())); + }, + ), + ListTile( + title: Text(L10().salesOrder), + subtitle: Text(L10().salesOrderSettings), + leading: Icon(TablerIcons.truck, color: COLOR_ACTION), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSalesOrderSettingsWidget())); + }, + ), Divider(), ListTile( title: Text(L10().about), diff --git a/lib/widget/attachment_widget.dart b/lib/widget/attachment_widget.dart index bc700f4d..e6cfcd75 100644 --- a/lib/widget/attachment_widget.dart +++ b/lib/widget/attachment_widget.dart @@ -26,11 +26,12 @@ import "package:inventree/widget/refreshable_state.dart"; */ class AttachmentWidget extends StatefulWidget { - const AttachmentWidget(this.attachmentClass, this.modelId, this.hasUploadPermission) : super(); + const AttachmentWidget(this.attachmentClass, this.modelId, this.imagePrefix, this.hasUploadPermission) : super(); final InvenTreeAttachment attachmentClass; final int modelId; final bool hasUploadPermission; + final String imagePrefix; @override _AttachmentWidgetState createState() => _AttachmentWidgetState(); @@ -54,6 +55,10 @@ class _AttachmentWidgetState extends RefreshableState { IconButton( icon: Icon(TablerIcons.camera), onPressed: () async { + widget.attachmentClass.uploadImage( + widget.modelId, + prefix: widget.imagePrefix, + ); FilePickerDialog.pickImageFromCamera().then((File? file) { upload(context, file); }); @@ -78,7 +83,6 @@ class _AttachmentWidgetState extends RefreshableState { final bool result = await widget.attachmentClass.uploadAttachment( file, - widget.attachmentClass.REF_MODEL_TYPE, widget.modelId ); diff --git a/lib/widget/company/company_detail.dart b/lib/widget/company/company_detail.dart index f79992b7..65f65d10 100644 --- a/lib/widget/company/company_detail.dart +++ b/lib/widget/company/company_detail.dart @@ -404,6 +404,7 @@ class _CompanyDetailState extends RefreshableState { builder: (context) => AttachmentWidget( InvenTreeCompanyAttachment(), widget.company.pk, + widget.company.name, InvenTreeCompany().canEdit ) ) diff --git a/lib/widget/order/purchase_order_detail.dart b/lib/widget/order/purchase_order_detail.dart index 6f413f59..29dc68bf 100644 --- a/lib/widget/order/purchase_order_detail.dart +++ b/lib/widget/order/purchase_order_detail.dart @@ -19,6 +19,7 @@ import "package:inventree/widget/progress.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/stock/stock_list.dart"; +import "package:inventree/preferences.dart"; /* @@ -45,6 +46,7 @@ class _PurchaseOrderDetailState extends RefreshableState actionButtons(BuildContext context) { List actions = []; + if (showCameraShortcut && widget.order.canEdit) { + actions.add( + SpeedDialChild( + child: Icon(TablerIcons.camera, color: Colors.blue), + label: L10().takePicture, + onTap: () async { + _uploadImage(context); + } + ) + ); + } + if (widget.order.canCreate) { if (widget.order.isPending) { @@ -137,6 +151,15 @@ class _PurchaseOrderDetailState extends RefreshableState _uploadImage(BuildContext context) async { + + InvenTreePurchaseOrderAttachment().uploadImage( + widget.order.pk, + prefix: widget.order.reference, + ).then((result) => refresh(context)); + } + /// Issue this order Future _issueOrder(BuildContext context) async { @@ -217,6 +240,7 @@ class _PurchaseOrderDetailState extends RefreshableState AttachmentWidget( InvenTreePurchaseOrderAttachment(), widget.order.pk, + widget.order.reference, widget.order.canEdit ) ) diff --git a/lib/widget/order/sales_order_detail.dart b/lib/widget/order/sales_order_detail.dart index 36b8214e..243a77f1 100644 --- a/lib/widget/order/sales_order_detail.dart +++ b/lib/widget/order/sales_order_detail.dart @@ -6,6 +6,7 @@ import "package:inventree/barcode/barcode.dart"; import "package:inventree/barcode/sales_order.dart"; import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/sales_order.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/widget/order/so_line_list.dart"; import "package:inventree/widget/order/so_shipment_list.dart"; import "package:inventree/widget/refreshable_state.dart"; @@ -40,6 +41,7 @@ class _SalesOrderDetailState extends RefreshableState { List lines = []; + bool showCameraShortcut = true; bool supportsProjectCodes = false; int attachmentCount = 0; @@ -100,6 +102,14 @@ class _SalesOrderDetailState extends RefreshableState { ); } + /// Upload an image for this order + Future _uploadImage(BuildContext context) async { + InvenTreeSalesOrderAttachment().uploadImage( + widget.order.pk, + prefix: widget.order.reference, + ).then((result) => refresh(context)); + } + /// Issue this order Future _issueOrder(BuildContext context) async { @@ -136,6 +146,18 @@ class _SalesOrderDetailState extends RefreshableState { List actionButtons(BuildContext context) { List actions = []; + if (showCameraShortcut && widget.order.canEdit) { + actions.add( + SpeedDialChild( + child: Icon(TablerIcons.camera, color: Colors.blue), + label: L10().takePicture, + onTap: () async { + _uploadImage(context); + } + ) + ); + } + if (widget.order.isPending) { actions.add( SpeedDialChild( @@ -231,6 +253,7 @@ class _SalesOrderDetailState extends RefreshableState { await api.SalesOrderStatus.load(); supportsProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED"); + showCameraShortcut = await InvenTreeSettingsManager().getBool(INV_SO_SHOW_CAMERA, true); InvenTreeSalesOrderAttachment().countAttachments(widget.order.pk).then((int value) { if (mounted) { @@ -378,6 +401,7 @@ class _SalesOrderDetailState extends RefreshableState { builder: (context) => AttachmentWidget( InvenTreeSalesOrderAttachment(), widget.order.pk, + widget.order.reference, widget.order.canEdit ) ) diff --git a/lib/widget/part/part_detail.dart b/lib/widget/part/part_detail.dart index c2d74705..4718e6eb 100644 --- a/lib/widget/part/part_detail.dart +++ b/lib/widget/part/part_detail.dart @@ -181,7 +181,7 @@ class _PartDisplayState extends RefreshableState { }); // Request the number of parameters for this part - showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; + showParameters = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_PARAMETERS, true); // Request the number of attachments InvenTreePartAttachment().countAttachments(part.pk).then((int value) { @@ -192,7 +192,7 @@ class _PartDisplayState extends RefreshableState { } }); - showBom = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_BOM, true) as bool; + showBom = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_BOM, true); // Request the number of BOM items InvenTreePart().count( @@ -588,6 +588,7 @@ class _PartDisplayState extends RefreshableState { builder: (context) => AttachmentWidget( InvenTreePartAttachment(), part.pk, + L10().part, part.canEdit ) ) diff --git a/lib/widget/stock/stock_detail.dart b/lib/widget/stock/stock_detail.dart index bf874b22..106d0509 100644 --- a/lib/widget/stock/stock_detail.dart +++ b/lib/widget/stock/stock_detail.dart @@ -844,6 +844,7 @@ class _StockItemDisplayState extends RefreshableState { builder: (context) => AttachmentWidget( InvenTreeStockItemAttachment(), widget.item.pk, + L10().stockItem, widget.item.canEdit, ) )