2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 13:36:50 +00:00

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
This commit is contained in:
Oliver 2024-12-05 14:38:53 +11:00 committed by GitHub
parent 4698e7e82c
commit 2e798b1bd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 321 additions and 17 deletions

View File

@ -1,6 +1,9 @@
### 0.17.0 - November 2024 ### 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 - Clearly indicate if a StockItem is unavailable
### 0.16.5 - September 2024 ### 0.16.5 - September 2024

View File

@ -12,7 +12,9 @@ import "package:inventree/api_form.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/inventree/sentry.dart"; import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart"; import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/fields.dart";
// Paginated response object // Paginated response object
@ -993,7 +995,7 @@ class InvenTreeAttachment extends InvenTreeModel {
return count(filters: filters); return count(filters: filters);
} }
Future<bool> uploadAttachment(File attachment, String modelType, int modelId, {String comment = "", Map<String, String> fields = const {}}) async { Future<bool> uploadAttachment(File attachment, int modelId, {String comment = "", Map<String, String> fields = const {}}) async {
// Ensure that the correct reference field is set // Ensure that the correct reference field is set
Map<String, String> data = Map<String, String>.from(fields); Map<String, String> data = Map<String, String>.from(fields);
@ -1002,14 +1004,9 @@ class InvenTreeAttachment extends InvenTreeModel {
if (InvenTreeAPI().supportsModernAttachments) { if (InvenTreeAPI().supportsModernAttachments) {
if (modelType.isEmpty) {
sentryReportMessage("uploadAttachment called with empty 'modelType'");
return false;
}
url = "attachment/"; url = "attachment/";
data["model_id"] = modelId.toString(); data["model_id"] = modelId.toString();
data["model_type"] = modelType; data["model_type"] = REF_MODEL_TYPE;
} else { } else {
@ -1032,6 +1029,41 @@ class InvenTreeAttachment extends InvenTreeModel {
return response.successful(); return response.successful();
} }
Future<bool> 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 * Download this attachment file
*/ */

View File

@ -897,6 +897,18 @@
"projectCode": "Project Code", "projectCode": "Project Code",
"@projectCode": {}, "@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": "Purchase Order",
"@purchaseOrder": {}, "@purchaseOrder": {},
@ -906,6 +918,9 @@
"purchaseOrderEdit": "Edit Purchase Order", "purchaseOrderEdit": "Edit Purchase Order",
"@purchaseOrderEdit": {}, "@purchaseOrderEdit": {},
"purchaseOrderSettings": "Purchase order settings",
"@purchaseOrderSettings": {},
"purchaseOrders": "Purchase Orders", "purchaseOrders": "Purchase Orders",
"@purchaseOrders": {}, "@purchaseOrders": {},
@ -1060,6 +1075,21 @@
"salesOrders": "Sales Orders", "salesOrders": "Sales Orders",
"@salesOrders": {}, "@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", "salesOrderCreate": "New Sales Order",
"@saleOrderCreate": {}, "@saleOrderCreate": {},

View File

@ -36,6 +36,14 @@ const String INV_STOCK_SHOW_HISTORY = "stockShowHistory";
const String INV_STOCK_SHOW_TESTS = "stockShowTests"; const String INV_STOCK_SHOW_TESTS = "stockShowTests";
const String INV_STOCK_CONFIRM_SCAN = "stockConfirmScan"; 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_REPORT_ERRORS = "reportErrors";
const String INV_STRICT_HTTPS = "strictHttps"; const String INV_STRICT_HTTPS = "strictHttps";

View File

@ -30,11 +30,11 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
} }
Future<void> loadSettings() async { Future<void> loadSettings() async {
partShowParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; partShowParameters = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_PARAMETERS, true);
partShowBom = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_BOM, true) as bool; partShowBom = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_BOM, true);
stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool; stockShowHistory = await InvenTreeSettingsManager().getBool(INV_STOCK_SHOW_HISTORY, false);
stockShowTests = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_TESTS, true) as bool; stockShowTests = await InvenTreeSettingsManager().getBool(INV_STOCK_SHOW_TESTS, true);
stockConfirmScan = await InvenTreeSettingsManager().getValue(INV_STOCK_CONFIRM_SCAN, false) as bool; stockConfirmScan = await InvenTreeSettingsManager().getBool(INV_STOCK_CONFIRM_SCAN, false);
if (mounted) { if (mounted) {
setState(() { setState(() {

View File

@ -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<InvenTreePurchaseOrderSettingsWidget> {
_InvenTreePurchaseOrderSettingsState();
bool poEnable = true;
bool poShowCamera = true;
@override
void initState() {
super.initState();
loadSettings();
}
Future<void> 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;
});
},
),
),
]
)
)
);
}
}

View File

@ -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<InvenTreeSalesOrderSettingsWidget> {
_InvenTreeSalesOrderSettingsState();
bool soEnable = true;
bool soShowCamera = true;
@override
void initState() {
super.initState();
loadSettings();
}
Future<void> 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;
});
},
),
),
]
)
)
);
}
}

View File

@ -11,7 +11,8 @@ import "package:inventree/settings/barcode_settings.dart";
import "package:inventree/settings/home_settings.dart"; import "package:inventree/settings/home_settings.dart";
import "package:inventree/settings/select_server.dart"; import "package:inventree/settings/select_server.dart";
import "package:inventree/settings/part_settings.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 // InvenTree settings view
class InvenTreeSettingsWidget extends StatefulWidget { class InvenTreeSettingsWidget extends StatefulWidget {
@ -86,6 +87,22 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePartSettingsWidget())); 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(), Divider(),
ListTile( ListTile(
title: Text(L10().about), title: Text(L10().about),

View File

@ -26,11 +26,12 @@ import "package:inventree/widget/refreshable_state.dart";
*/ */
class AttachmentWidget extends StatefulWidget { 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 InvenTreeAttachment attachmentClass;
final int modelId; final int modelId;
final bool hasUploadPermission; final bool hasUploadPermission;
final String imagePrefix;
@override @override
_AttachmentWidgetState createState() => _AttachmentWidgetState(); _AttachmentWidgetState createState() => _AttachmentWidgetState();
@ -54,6 +55,10 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
IconButton( IconButton(
icon: Icon(TablerIcons.camera), icon: Icon(TablerIcons.camera),
onPressed: () async { onPressed: () async {
widget.attachmentClass.uploadImage(
widget.modelId,
prefix: widget.imagePrefix,
);
FilePickerDialog.pickImageFromCamera().then((File? file) { FilePickerDialog.pickImageFromCamera().then((File? file) {
upload(context, file); upload(context, file);
}); });
@ -78,7 +83,6 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
final bool result = await widget.attachmentClass.uploadAttachment( final bool result = await widget.attachmentClass.uploadAttachment(
file, file,
widget.attachmentClass.REF_MODEL_TYPE,
widget.modelId widget.modelId
); );

View File

@ -404,6 +404,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
builder: (context) => AttachmentWidget( builder: (context) => AttachmentWidget(
InvenTreeCompanyAttachment(), InvenTreeCompanyAttachment(),
widget.company.pk, widget.company.pk,
widget.company.name,
InvenTreeCompany().canEdit InvenTreeCompany().canEdit
) )
) )

View File

@ -19,6 +19,7 @@ import "package:inventree/widget/progress.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock/stock_list.dart"; import "package:inventree/widget/stock/stock_list.dart";
import "package:inventree/preferences.dart";
/* /*
@ -45,6 +46,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
int attachmentCount = 0; int attachmentCount = 0;
bool showCameraShortcut = true;
bool supportProjectCodes = false; bool supportProjectCodes = false;
@override @override
@ -73,6 +75,18 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
List<SpeedDialChild> actionButtons(BuildContext context) { List<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> actions = []; List<SpeedDialChild> 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.canCreate) {
if (widget.order.isPending) { if (widget.order.isPending) {
@ -137,6 +151,15 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
); );
} }
/// Upload an image against the current PurchaseOrder
Future<void> _uploadImage(BuildContext context) async {
InvenTreePurchaseOrderAttachment().uploadImage(
widget.order.pk,
prefix: widget.order.reference,
).then((result) => refresh(context));
}
/// Issue this order /// Issue this order
Future<void> _issueOrder(BuildContext context) async { Future<void> _issueOrder(BuildContext context) async {
@ -217,6 +240,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
lines = await widget.order.getLineItems(); lines = await widget.order.getLineItems();
showCameraShortcut = await InvenTreeSettingsManager().getBool(INV_PO_SHOW_CAMERA, true);
supportProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED"); supportProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED");
completedLines = 0; completedLines = 0;
@ -389,6 +413,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
builder: (context) => AttachmentWidget( builder: (context) => AttachmentWidget(
InvenTreePurchaseOrderAttachment(), InvenTreePurchaseOrderAttachment(),
widget.order.pk, widget.order.pk,
widget.order.reference,
widget.order.canEdit widget.order.canEdit
) )
) )

View File

@ -6,6 +6,7 @@ import "package:inventree/barcode/barcode.dart";
import "package:inventree/barcode/sales_order.dart"; import "package:inventree/barcode/sales_order.dart";
import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/sales_order.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_line_list.dart";
import "package:inventree/widget/order/so_shipment_list.dart"; import "package:inventree/widget/order/so_shipment_list.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
@ -40,6 +41,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
List<InvenTreeSOLineItem> lines = []; List<InvenTreeSOLineItem> lines = [];
bool showCameraShortcut = true;
bool supportsProjectCodes = false; bool supportsProjectCodes = false;
int attachmentCount = 0; int attachmentCount = 0;
@ -100,6 +102,14 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
); );
} }
/// Upload an image for this order
Future<void> _uploadImage(BuildContext context) async {
InvenTreeSalesOrderAttachment().uploadImage(
widget.order.pk,
prefix: widget.order.reference,
).then((result) => refresh(context));
}
/// Issue this order /// Issue this order
Future<void> _issueOrder(BuildContext context) async { Future<void> _issueOrder(BuildContext context) async {
@ -136,6 +146,18 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
List<SpeedDialChild> actionButtons(BuildContext context) { List<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> actions = []; List<SpeedDialChild> 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) { if (widget.order.isPending) {
actions.add( actions.add(
SpeedDialChild( SpeedDialChild(
@ -231,6 +253,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
await api.SalesOrderStatus.load(); await api.SalesOrderStatus.load();
supportsProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED"); 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) { InvenTreeSalesOrderAttachment().countAttachments(widget.order.pk).then((int value) {
if (mounted) { if (mounted) {
@ -378,6 +401,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
builder: (context) => AttachmentWidget( builder: (context) => AttachmentWidget(
InvenTreeSalesOrderAttachment(), InvenTreeSalesOrderAttachment(),
widget.order.pk, widget.order.pk,
widget.order.reference,
widget.order.canEdit widget.order.canEdit
) )
) )

View File

@ -181,7 +181,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
}); });
// Request the number of parameters for this part // 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 // Request the number of attachments
InvenTreePartAttachment().countAttachments(part.pk).then((int value) { InvenTreePartAttachment().countAttachments(part.pk).then((int value) {
@ -192,7 +192,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
} }
}); });
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 // Request the number of BOM items
InvenTreePart().count( InvenTreePart().count(
@ -588,6 +588,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
builder: (context) => AttachmentWidget( builder: (context) => AttachmentWidget(
InvenTreePartAttachment(), InvenTreePartAttachment(),
part.pk, part.pk,
L10().part,
part.canEdit part.canEdit
) )
) )

View File

@ -844,6 +844,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
builder: (context) => AttachmentWidget( builder: (context) => AttachmentWidget(
InvenTreeStockItemAttachment(), InvenTreeStockItemAttachment(),
widget.item.pk, widget.item.pk,
L10().stockItem,
widget.item.canEdit, widget.item.canEdit,
) )
) )