From 571b49184684c34df3c51ff7b4deb024c7eaf89b Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Dec 2023 23:49:55 +1100 Subject: [PATCH] Sales order barcode (#466) * Add barcode handler for allocating stock to sales order * Refactor sales order allocation fields * Improve barcode handling * Handle barcode scan from sales order line detail view * Remove debug statements --- lib/api.dart | 3 + lib/barcode/sales_order.dart | 95 ++++++++++++++++++++++++ lib/inventree/sales_order.dart | 23 ++++++ lib/widget/order/sales_order_detail.dart | 17 +++++ lib/widget/order/so_line_detail.dart | 66 +++++++++------- 5 files changed, 179 insertions(+), 25 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 00833f0b..e2e1f1df 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -336,6 +336,9 @@ class InvenTreeAPI { // Does the server support adding line items to a PO using barcodes? bool get supportsBarcodePOAddLineEndpoint => isConnected() && apiVersion >= 153; + // Does the server support allocating stock to sales order using barcodes? + bool get supportsBarcodeSOAllocateEndpoint => isConnected() && apiVersion >= 160; + // Cached list of plugins (refreshed when we connect to the server) List _plugins = []; diff --git a/lib/barcode/sales_order.dart b/lib/barcode/sales_order.dart index d485f8fc..69583d91 100644 --- a/lib/barcode/sales_order.dart +++ b/lib/barcode/sales_order.dart @@ -1,4 +1,6 @@ import "package:flutter/material.dart"; +import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:inventree/api_form.dart"; import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/sales_order.dart"; @@ -77,5 +79,98 @@ class SOAddItemBarcodeHandler extends BarcodeHandler { } } +} + +class SOAllocateStockHandler extends BarcodeHandler { + + SOAllocateStockHandler({this.salesOrder, this.lineItem, this.shipment}); + + InvenTreeSalesOrder? salesOrder; + InvenTreeSOLineItem? lineItem; + InvenTreeSalesOrderShipment? shipment; + + @override + String getOverlayText(BuildContext context) => L10().allocateStock; + + @override + Future processBarcode(String barcode, + { + String url = "barcode/so-allocate/", + Map extra_data = const {}}) { + + final so_extra_data = { + "sales_order": salesOrder?.pk, + "shipment": shipment?.pk, + "line": lineItem?.pk, + ...extra_data + }; + + return super.processBarcode(barcode, url: url, extra_data: so_extra_data); + } + + @override + Future onBarcodeMatched(Map data) async { + if (!data.containsKey("line_item")) { + return onBarcodeUnknown(data); + } + + barcodeSuccessTone(); + showSnackIcon(L10().allocated, success: true); + } + + @override + Future onBarcodeUnhandled(Map data) async { + + if (!data.containsKey("action_required") || !data.containsKey("line_item")) { + return super.onBarcodeUnhandled(data); + } + + // Prompt user for extra information to create the allocation + var fields = InvenTreeSOLineItem().allocateFormFields(); + + // Update fields with data gathered from the API response + fields["line_item"]?["value"] = data["line_item"]; + + Map stock_filters = { + "in_stock": true, + "available": true, + }; + + if (data.containsKey("part")) { + stock_filters["part"] = data["part"]; + } + + fields["stock_item"]?["filters"] = stock_filters; + fields["stock_item"]?["value"] = data["stock_item"]; + + fields["quantity"]?["value"] = data["quantity"]; + + fields["shipment"]?["value"] = data["shipment"]; + fields["shipment"]?["filters"] = { + "order": salesOrder!.pk.toString() + }; + + final context = OneContext().context!; + + launchApiForm( + context, + L10().allocateStock, + salesOrder!.allocate_url, + fields, + method: "POST", + icon: FontAwesomeIcons.rightToBracket, + onSuccess: (data) async { + showSnackIcon(L10().allocated, success: true); + }); + } + + @override + Future onBarcodeUnknown(Map data) async { + barcodeFailureTone(); + showSnackIcon( + data["error"] as String? ?? L10().barcodeError, + success: false + ); + } } \ No newline at end of file diff --git a/lib/inventree/sales_order.dart b/lib/inventree/sales_order.dart index 709ddceb..b65ad611 100644 --- a/lib/inventree/sales_order.dart +++ b/lib/inventree/sales_order.dart @@ -132,6 +132,29 @@ class InvenTreeSOLineItem extends InvenTreeOrderLine { }; } + Map> allocateFormFields() { + + return { + "line_item": { + "parent": "items", + "nested": true, + "hidden": true, + }, + "stock_item": { + "parent": "items", + "nested": true, + "filters": {}, + }, + "quantity": { + "parent": "items", + "nested": true, + }, + "shipment": { + "filters": {} + } + }; + } + @override Map defaultGetFilters() { return { diff --git a/lib/widget/order/sales_order_detail.dart b/lib/widget/order/sales_order_detail.dart index 0f099616..bca7e518 100644 --- a/lib/widget/order/sales_order_detail.dart +++ b/lib/widget/order/sales_order_detail.dart @@ -146,6 +146,23 @@ class _SalesOrderDetailState extends RefreshableState { } ) ); + + if (api.supportsBarcodeSOAllocateEndpoint) { + actions.add( + SpeedDialChild( + child: FaIcon(FontAwesomeIcons.rightToBracket), + label: L10().allocateStock, + onTap: () async { + scanBarcode( + context, + handler: SOAllocateStockHandler( + salesOrder: widget.order, + ) + ); + } + ) + ); + } } return actions; diff --git a/lib/widget/order/so_line_detail.dart b/lib/widget/order/so_line_detail.dart index b5d88e74..a4168980 100644 --- a/lib/widget/order/so_line_detail.dart +++ b/lib/widget/order/so_line_detail.dart @@ -7,6 +7,8 @@ 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/barcode/barcode.dart"; +import "package:inventree/barcode/sales_order.dart"; import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/sales_order.dart"; @@ -66,31 +68,17 @@ class _SOLineDetailWidgetState extends RefreshableState { return; } - Map fields = { - "line_item": { - "parent": "items", - "nested": true, - "hidden": true, - "value": widget.item.pk, - }, - "stock_item": { - "parent": "items", - "nested": true, - "filters": { - "part": widget.item.partId, - "in_stock": true, - } - }, - "quantity": { - "parent": "items", - "nested": true, - "value": widget.item.unallocatedQuantity, - }, - "shipment": { - "filters": { - "order": order!.pk.toString(), - } - }, + var fields = InvenTreeSOLineItem().allocateFormFields(); + + fields["line_item"]?["value"] = widget.item.pk.toString(); + fields["stock_item"]?["filters"] = { + "in_stock": true, + "available": true, + "part": widget.item.partId.toString() + }; + fields["quantity"]?["value"] = widget.item.unallocatedQuantity.toString(); + fields["shipment"]?["filters"] = { + "order": order!.pk.toString() }; launchApiForm( @@ -146,6 +134,34 @@ class _SOLineDetailWidgetState extends RefreshableState { return buttons; } + @override + List barcodeButtons(BuildContext context) { + List actions = []; + + if (order != null && order!.isOpen && InvenTreeSOLineItem().canCreate) { + + if (api.supportsBarcodeSOAllocateEndpoint) { + actions.add( + SpeedDialChild( + child: FaIcon(FontAwesomeIcons.rightToBracket), + label: L10().allocateStock, + onTap: () async { + scanBarcode( + context, + handler: SOAllocateStockHandler( + salesOrder: order, + lineItem: widget.item + ) + ); + } + ) + ); + } + } + + return actions; + } + @override Future request(BuildContext context) async { await widget.item.reload();