diff --git a/lib/api.dart b/lib/api.dart index ba641710..6ea78805 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -328,6 +328,8 @@ class InvenTreeAPI { // Does the server support extra fields on stock adjustment actions? bool get supportsStockAdjustExtraFields => isConnected() && apiVersion >= 133; + bool get supportsBarcodePOReceiveEndpoint => isConnected() && apiVersion >= 139; + // Are plugins enabled on the server? bool _pluginsEnabled = false; diff --git a/lib/barcode/barcode.dart b/lib/barcode/barcode.dart index d87f34c3..6a2a069c 100644 --- a/lib/barcode/barcode.dart +++ b/lib/barcode/barcode.dart @@ -6,6 +6,7 @@ import "package:one_context/one_context.dart"; import "package:inventree/api.dart"; +import "package:inventree/api_form.dart"; import "package:inventree/helpers.dart"; import "package:inventree/l10.dart"; @@ -462,6 +463,116 @@ class ScanParentLocationHandler extends BarcodeScanStockLocationHandler { } +/* + * Barcode handler class for scanning a supplier barcode to receive a part + * + * - The class can be initialized by optionally passing a valid, placed PurchaseOrder object + * - Expects to scan supplier barcode, possibly containing order_number and quantity + * - If location or quantity information wasn't provided, show a form to fill it in + */ +class POReceiveBarcodeHandler extends BarcodeHandler { + + POReceiveBarcodeHandler({this.purchaseOrder, this.location}); + + InvenTreePurchaseOrder? purchaseOrder; + InvenTreeStockLocation? location; + + @override + String getOverlayText(BuildContext context) => L10().barcodeReceivePart; + + @override + Future processBarcode(String barcode, + {String url = "barcode/po-receive/", + Map extra_data = const {}}) { + + final po_extra_data = { + "purchase_order": purchaseOrder?.pk, + "location": location?.pk, + ...extra_data, + }; + + return super.processBarcode(barcode, url: url, extra_data: po_extra_data); + } + + @override + Future onBarcodeMatched(Map data) async { + if (!data.containsKey("lineitem")) { + return onBarcodeUnknown(data); + } + + barcodeSuccessTone(); + showSnackIcon(L10().receivedItem, success: true); + } + + @override + Future onBarcodeUnhandled(Map data) async { + if (!data.containsKey("action_required") || !data.containsKey("lineitem")) { + return super.onBarcodeUnhandled(data); + } + + final lineItemData = data["lineitem"] as Map; + if (!lineItemData.containsKey("pk") || !lineItemData.containsKey("purchase_order")) { + barcodeFailureTone(); + showSnackIcon(L10().missingData, success: false); + } + + // Construct fields to receive + Map fields = { + "line_item": { + "parent": "items", + "nested": true, + "hidden": true, + "value": lineItemData["pk"] as int, + }, + "quantity": { + "parent": "items", + "nested": true, + "value": lineItemData["quantity"] as double?, + }, + "status": { + "parent": "items", + "nested": true, + }, + "location": { + "value": lineItemData["location"] as int?, + }, + "barcode": { + "parent": "items", + "nested": true, + "hidden": true, + "type": "barcode", + "value": data["barcode_data"] as String, + } + }; + + final context = OneContext().context!; + final purchase_order_pk = lineItemData["purchase_order"]; + final receive_url = "${InvenTreePurchaseOrder().URL}${purchase_order_pk}/receive/"; + + launchApiForm( + context, + L10().receiveItem, + receive_url, + fields, + method: "POST", + icon: FontAwesomeIcons.rightToBracket, + onSuccess: (data) async { + showSnackIcon(L10().receivedItem, success: true); + } + ); + } + + @override + Future onBarcodeUnknown(Map data) async { + barcodeFailureTone(); + showSnackIcon( + data["error"] as String? ?? L10().barcodeError, + success: false + ); + } +} + + /* * Barcode handler for finding a "unique" barcode (one that does not match an item in the database) */ diff --git a/lib/barcode/handler.dart b/lib/barcode/handler.dart index 3d72e9c9..5048f635 100644 --- a/lib/barcode/handler.dart +++ b/lib/barcode/handler.dart @@ -58,8 +58,9 @@ class BarcodeHandler { * * Returns true only if the barcode scanner should remain open */ - Future processBarcode(String barcode, {String url = "barcode/"}) async { - + Future processBarcode(String barcode, + {String url = "barcode/", + Map extra_data = const {}}) async { debug("Scanned barcode data: '${barcode}'"); barcode = barcode.trim(); @@ -82,6 +83,7 @@ class BarcodeHandler { url, body: { "barcode": barcode, + ...extra_data, }, expectedStatusCode: null, // Do not show an error on "unexpected code" ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9589f9ba..673d284f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -112,6 +112,9 @@ "barcodeNotAssigned": "Barcode not assigned", "@barcodeNotAssigned": {}, + "barcodeReceivePart": "Scan barcode to receive part", + "@barcodeReceivePart": {}, + "barcodeScanAssign": "Scan to assign barcode", "@barcodeScanAssign": {}, @@ -988,6 +991,9 @@ "scanIntoLocationDetail": "Scan this item into location", "@scanIntoLocationDetail": {}, + "scanReceivedParts": "Scan Received Parts", + "@scanReceivedParts": {}, + "search": "Search", "@search": { "description": "search" diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 0ca1c5dd..4e89f86a 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -104,6 +104,21 @@ class _LocationDisplayState extends RefreshableState { ); } + if (api.supportsBarcodePOReceiveEndpoint) { + actions.add( + SpeedDialChild( + child: Icon(Icons.barcode_reader), + label: L10().scanReceivedParts, + onTap:() async { + scanBarcode( + context, + handler: POReceiveBarcodeHandler(location: location), + ); + }, + ) + ); + } + // Scan this location into another one if (InvenTreeStockLocation().canEdit) { actions.add( diff --git a/lib/widget/purchase_order_detail.dart b/lib/widget/purchase_order_detail.dart index ba870a49..f03d4f0b 100644 --- a/lib/widget/purchase_order_detail.dart +++ b/lib/widget/purchase_order_detail.dart @@ -6,6 +6,7 @@ import "package:inventree/widget/po_line_list.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; +import "package:inventree/barcode/barcode.dart"; import "package:inventree/helpers.dart"; import "package:inventree/l10.dart"; @@ -132,6 +133,29 @@ class _PurchaseOrderDetailState extends RefreshableState barcodeButtons(BuildContext context) { + List actions = []; + + if (api.supportsBarcodePOReceiveEndpoint) { + actions.add( + SpeedDialChild( + child: Icon(Icons.barcode_reader), + label: L10().scanReceivedParts, + onTap:() async { + scanBarcode( + context, + handler: POReceiveBarcodeHandler(purchaseOrder: order), + ); + }, + ) + ); + } + + return actions; + } + + @override Future request(BuildContext context) async { await order.reload(); diff --git a/lib/widget/purchase_order_list.dart b/lib/widget/purchase_order_list.dart index cdb8b8be..44046564 100644 --- a/lib/widget/purchase_order_list.dart +++ b/lib/widget/purchase_order_list.dart @@ -9,6 +9,7 @@ import "package:inventree/widget/purchase_order_detail.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/l10.dart"; import "package:inventree/api.dart"; +import "package:inventree/barcode/barcode.dart"; import "package:inventree/inventree/purchase_order.dart"; /* @@ -79,6 +80,28 @@ class _PurchaseOrderListWidgetState extends RefreshableState barcodeButtons(BuildContext context) { + List actions = []; + + if (api.supportsBarcodePOReceiveEndpoint) { + actions.add( + SpeedDialChild( + child: Icon(Icons.barcode_reader), + label: L10().scanReceivedParts, + onTap:() async { + scanBarcode( + context, + handler: POReceiveBarcodeHandler(), + ); + }, + ) + ); + } + + return actions; + } + @override Widget getBody(BuildContext context) { return PaginatedPurchaseOrderList(filters);