2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-27 21:16:48 +00:00

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
This commit is contained in:
Oliver 2023-11-20 23:48:42 +11:00 committed by GitHub
parent 0a85441131
commit bf3df770c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 250 additions and 114 deletions

View File

@ -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<InvenTreePlugin> _plugins = [];

View File

@ -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<void> 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<void> onBarcodeMatched(Map<String, dynamic> 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<void> processBarcode(String barcode,
{String url = "barcode/po-receive/",
Map<String, dynamic> 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<void> onBarcodeMatched(Map<String, dynamic> data) async {
if (!data.containsKey("lineitem")) {
return onBarcodeUnknown(data);
}
barcodeSuccessTone();
showSnackIcon(L10().receivedItem, success: true);
}
@override
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
if (!data.containsKey("action_required") || !data.containsKey("lineitem")) {
return super.onBarcodeUnhandled(data);
}
final lineItemData = data["lineitem"] as Map<String, dynamic>;
if (!lineItemData.containsKey("pk") || !lineItemData.containsKey("purchase_order")) {
barcodeFailureTone();
showSnackIcon(L10().missingData, success: false);
}
// Construct fields to receive
Map<String, dynamic> 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<void> onBarcodeUnknown(Map<String, dynamic> 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)
*/

View File

@ -41,7 +41,7 @@ class BarcodeHandler {
barcodeFailureTone();
showSnackIcon(
L10().barcodeNoMatch,
(data["error"] ?? L10().barcodeNoMatch) as String,
success: false,
icon: Icons.qr_code,
);

View File

@ -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<void> processBarcode(String barcode,
{String url = "barcode/po-receive/",
Map<String, dynamic> 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<void> onBarcodeMatched(Map<String, dynamic> data) async {
if (!data.containsKey("lineitem")) {
return onBarcodeUnknown(data);
}
barcodeSuccessTone();
showSnackIcon(L10().receivedItem, success: true);
}
@override
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
if (!data.containsKey("action_required") || !data.containsKey("lineitem")) {
return super.onBarcodeUnhandled(data);
}
final lineItemData = data["lineitem"] as Map<String, dynamic>;
if (!lineItemData.containsKey("pk") || !lineItemData.containsKey("purchase_order")) {
barcodeFailureTone();
showSnackIcon(L10().missingData, success: false);
}
// Construct fields to receive
Map<String, dynamic> 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<void> onBarcodeUnknown(Map<String, dynamic> 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<void> processBarcode(String barcode, {
String url = "barcode/po-allocate/",
Map<String, dynamic> 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<void> onBarcodeMatched(Map<String, dynamic> 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<String, dynamic>) {
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<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
print("onBarcodeUnhandled:");
print(data.toString());
super.onBarcodeUnhandled(data);
}
}

View File

@ -1057,6 +1057,9 @@
"@scanBarcode": {
},
"scanSupplierPart": "Scan supplier part barcode",
"@scanSupplierPart": {},
"scanIntoLocation": "Scan Into Location",
"@scanIntoLocation": {},

View File

@ -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<PurchaseOrderDetailWidg
List<SpeedDialChild> barcodeButtons(BuildContext context) {
List<SpeedDialChild> 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<PurchaseOrderDetailWidg
scanBarcode(
context,
handler: POReceiveBarcodeHandler(purchaseOrder: widget.order),
);
).then((value) {
refresh(context);
});
},
)
);
}
if (widget.order.isPending && api.supportsBarcodePOAddLineEndpoint) {
actions.add(
SpeedDialChild(
child: FaIcon(FontAwesomeIcons.circlePlus, color: COLOR_SUCCESS),
label: L10().lineItemAdd,
onTap: () async {
scanBarcode(
context,
handler: POAllocateBarcodeHandler(purchaseOrder: widget.order),
);
}
)
);
}
return actions;
}

View File

@ -10,6 +10,7 @@ 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/barcode/purchase_order.dart";
import "package:inventree/inventree/purchase_order.dart";
/*

View File

@ -5,6 +5,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/barcode/barcode.dart";
import "package:inventree/barcode/purchase_order.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/stock.dart";