From bf3df770c7ca9484a73a039eb9a53224977c80aa Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 20 Nov 2023 23:48:42 +1100 Subject: [PATCH] Po barcode scan (#458) * Refactor existing barcode scan endpoint - Break out into new file just for purchase orders * Handle scanning of salesorder * Add new handler for adding items to PO via barcode * Allocate with barcode * Add new string --- lib/api.dart | 4 + lib/barcode/barcode.dart | 128 ++---------- lib/barcode/handler.dart | 2 +- lib/barcode/purchase_order.dart | 203 ++++++++++++++++++++ lib/l10n/app_en.arb | 3 + lib/widget/order/purchase_order_detail.dart | 22 ++- lib/widget/order/purchase_order_list.dart | 1 + lib/widget/stock/location_display.dart | 1 + 8 files changed, 250 insertions(+), 114 deletions(-) create mode 100644 lib/barcode/purchase_order.dart diff --git a/lib/api.dart b/lib/api.dart index 28f85f39..00833f0b 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -330,8 +330,12 @@ class InvenTreeAPI { // Does the server support extra fields on stock adjustment actions? bool get supportsStockAdjustExtraFields => isConnected() && apiVersion >= 133; + // Does the server support receiving items against a PO using barcodes? bool get supportsBarcodePOReceiveEndpoint => isConnected() && apiVersion >= 139; + // Does the server support adding line items to a PO using barcodes? + bool get supportsBarcodePOAddLineEndpoint => isConnected() && apiVersion >= 153; + // Cached list of plugins (refreshed when we connect to the server) List _plugins = []; diff --git a/lib/barcode/barcode.dart b/lib/barcode/barcode.dart index 4ea044e1..91e600c4 100644 --- a/lib/barcode/barcode.dart +++ b/lib/barcode/barcode.dart @@ -2,12 +2,13 @@ import "package:flutter/material.dart"; import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:inventree/inventree/sales_order.dart"; import "package:inventree/preferences.dart"; +import "package:inventree/widget/order/sales_order_detail.dart"; 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"; @@ -166,6 +167,16 @@ class BarcodeScanHandler extends BarcodeHandler { } } + // Response when a SalesOrder instance is scanned + Future handleSalesOrder(int pk) async { + var order = await InvenTreeSalesOrder().get(pk); + + if (order is InvenTreeSalesOrder) { + OneContext().pop(); + OneContext().push(MaterialPageRoute( + builder: (context) => SalesOrderDetailWidget(order))); + } + } @override Future onBarcodeMatched(Map data) async { @@ -184,6 +195,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (InvenTreeAPI().supportsOrderBarcodes) { validModels.add("purchaseorder"); + validModels.add("salesorder"); } for (var key in validModels) { @@ -219,6 +231,10 @@ class BarcodeScanHandler extends BarcodeHandler { case "purchaseorder": await handlePurchaseOrder(pk); return; + case "salesorder": + await handleSalesOrder(pk); + return; + // TODO: Handle manufacturer part default: // Fall through to failure state break; @@ -478,116 +494,6 @@ 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 5048f635..3dae4847 100644 --- a/lib/barcode/handler.dart +++ b/lib/barcode/handler.dart @@ -41,7 +41,7 @@ class BarcodeHandler { barcodeFailureTone(); showSnackIcon( - L10().barcodeNoMatch, + (data["error"] ?? L10().barcodeNoMatch) as String, success: false, icon: Icons.qr_code, ); diff --git a/lib/barcode/purchase_order.dart b/lib/barcode/purchase_order.dart new file mode 100644 index 00000000..3069da20 --- /dev/null +++ b/lib/barcode/purchase_order.dart @@ -0,0 +1,203 @@ +import "package:flutter/material.dart"; + +import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:one_context/one_context.dart"; + +import "package:inventree/l10.dart"; +import "package:inventree/api_form.dart"; + +import "package:inventree/barcode/handler.dart"; +import "package:inventree/barcode/tones.dart"; + +import "package:inventree/inventree/purchase_order.dart"; +import "package:inventree/inventree/stock.dart"; + +import "package:inventree/widget/snacks.dart"; + +/* + * 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 to add a line item to a purchase order + */ +class POAllocateBarcodeHandler extends BarcodeHandler { + + POAllocateBarcodeHandler({this.purchaseOrder}); + + InvenTreePurchaseOrder? purchaseOrder; + + @override + String getOverlayText(BuildContext context) => L10().scanSupplierPart; + + @override + Future processBarcode(String barcode, { + String url = "barcode/po-allocate/", + Map extra_data = const {}} + ) { + + final po_extra_data = { + "purchase_order": purchaseOrder?.pk, + ...extra_data, + }; + + return super.processBarcode( + barcode, + url: url, + extra_data: po_extra_data, + ); + } + + @override + Future onBarcodeMatched(Map data) async { + // Server must respond with a suppliertpart instance + if (!data.containsKey("supplierpart")) { + return onBarcodeUnknown(data); + } + + dynamic supplier_part = data["supplierpart"]; + + int supplier_part_pk = -1; + + if (supplier_part is Map) { + supplier_part_pk = (supplier_part["pk"] ?? -1) as int; + } else { + return onBarcodeUnknown(data); + } + + // Dispose of the barcode scanner + if (OneContext.hasContext) { + OneContext().pop(); + } + + final context = OneContext().context!; + + var fields = InvenTreePOLineItem().formFields(); + + fields["order"]?["value"] = purchaseOrder!.pk; + fields["part"]?["hidden"] = false; + fields["part"]?["value"] = supplier_part_pk; + + InvenTreePOLineItem().createForm( + context, + L10().lineItemAdd, + fields: fields, + onSuccess: (data) async {}, + ); + } + + @override + Future onBarcodeUnhandled(Map data) async { + + print("onBarcodeUnhandled:"); + print(data.toString()); + + super.onBarcodeUnhandled(data); + } +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4ca99a65..e26cfab1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1057,6 +1057,9 @@ "@scanBarcode": { }, + "scanSupplierPart": "Scan supplier part barcode", + "@scanSupplierPart": {}, + "scanIntoLocation": "Scan Into Location", "@scanIntoLocation": {}, diff --git a/lib/widget/order/purchase_order_detail.dart b/lib/widget/order/purchase_order_detail.dart index 13cb7a4f..ae7606ad 100644 --- a/lib/widget/order/purchase_order_detail.dart +++ b/lib/widget/order/purchase_order_detail.dart @@ -6,6 +6,7 @@ import "package:inventree/widget/order/po_line_list.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/barcode/barcode.dart"; +import "package:inventree/barcode/purchase_order.dart"; import "package:inventree/helpers.dart"; import "package:inventree/l10.dart"; @@ -173,7 +174,7 @@ class _PurchaseOrderDetailState extends RefreshableState barcodeButtons(BuildContext context) { List actions = []; - if (api.supportsBarcodePOReceiveEndpoint) { + if (api.supportsBarcodePOReceiveEndpoint && widget.order.isPlaced) { actions.add( SpeedDialChild( child: Icon(Icons.barcode_reader), @@ -182,12 +183,29 @@ class _PurchaseOrderDetailState extends RefreshableState