mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 13:36:50 +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:
parent
0a85441131
commit
bf3df770c7
@ -330,8 +330,12 @@ class InvenTreeAPI {
|
|||||||
// Does the server support extra fields on stock adjustment actions?
|
// Does the server support extra fields on stock adjustment actions?
|
||||||
bool get supportsStockAdjustExtraFields => isConnected() && apiVersion >= 133;
|
bool get supportsStockAdjustExtraFields => isConnected() && apiVersion >= 133;
|
||||||
|
|
||||||
|
// Does the server support receiving items against a PO using barcodes?
|
||||||
bool get supportsBarcodePOReceiveEndpoint => isConnected() && apiVersion >= 139;
|
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)
|
// Cached list of plugins (refreshed when we connect to the server)
|
||||||
List<InvenTreePlugin> _plugins = [];
|
List<InvenTreePlugin> _plugins = [];
|
||||||
|
|
||||||
|
@ -2,12 +2,13 @@ import "package:flutter/material.dart";
|
|||||||
|
|
||||||
import "package:flutter_speed_dial/flutter_speed_dial.dart";
|
import "package:flutter_speed_dial/flutter_speed_dial.dart";
|
||||||
import "package:font_awesome_flutter/font_awesome_flutter.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/preferences.dart";
|
||||||
|
import "package:inventree/widget/order/sales_order_detail.dart";
|
||||||
import "package:one_context/one_context.dart";
|
import "package:one_context/one_context.dart";
|
||||||
|
|
||||||
|
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/api_form.dart";
|
|
||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
import "package:inventree/l10.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
|
@override
|
||||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||||
@ -184,6 +195,7 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
|
|
||||||
if (InvenTreeAPI().supportsOrderBarcodes) {
|
if (InvenTreeAPI().supportsOrderBarcodes) {
|
||||||
validModels.add("purchaseorder");
|
validModels.add("purchaseorder");
|
||||||
|
validModels.add("salesorder");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var key in validModels) {
|
for (var key in validModels) {
|
||||||
@ -219,6 +231,10 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
case "purchaseorder":
|
case "purchaseorder":
|
||||||
await handlePurchaseOrder(pk);
|
await handlePurchaseOrder(pk);
|
||||||
return;
|
return;
|
||||||
|
case "salesorder":
|
||||||
|
await handleSalesOrder(pk);
|
||||||
|
return;
|
||||||
|
// TODO: Handle manufacturer part
|
||||||
default:
|
default:
|
||||||
// Fall through to failure state
|
// Fall through to failure state
|
||||||
break;
|
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)
|
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
||||||
*/
|
*/
|
||||||
|
@ -41,7 +41,7 @@ class BarcodeHandler {
|
|||||||
barcodeFailureTone();
|
barcodeFailureTone();
|
||||||
|
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
L10().barcodeNoMatch,
|
(data["error"] ?? L10().barcodeNoMatch) as String,
|
||||||
success: false,
|
success: false,
|
||||||
icon: Icons.qr_code,
|
icon: Icons.qr_code,
|
||||||
);
|
);
|
||||||
|
203
lib/barcode/purchase_order.dart
Normal file
203
lib/barcode/purchase_order.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1057,6 +1057,9 @@
|
|||||||
"@scanBarcode": {
|
"@scanBarcode": {
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"scanSupplierPart": "Scan supplier part barcode",
|
||||||
|
"@scanSupplierPart": {},
|
||||||
|
|
||||||
"scanIntoLocation": "Scan Into Location",
|
"scanIntoLocation": "Scan Into Location",
|
||||||
"@scanIntoLocation": {},
|
"@scanIntoLocation": {},
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import "package:inventree/widget/order/po_line_list.dart";
|
|||||||
|
|
||||||
import "package:inventree/app_colors.dart";
|
import "package:inventree/app_colors.dart";
|
||||||
import "package:inventree/barcode/barcode.dart";
|
import "package:inventree/barcode/barcode.dart";
|
||||||
|
import "package:inventree/barcode/purchase_order.dart";
|
||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
@ -173,7 +174,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
|
|||||||
List<SpeedDialChild> barcodeButtons(BuildContext context) {
|
List<SpeedDialChild> barcodeButtons(BuildContext context) {
|
||||||
List<SpeedDialChild> actions = [];
|
List<SpeedDialChild> actions = [];
|
||||||
|
|
||||||
if (api.supportsBarcodePOReceiveEndpoint) {
|
if (api.supportsBarcodePOReceiveEndpoint && widget.order.isPlaced) {
|
||||||
actions.add(
|
actions.add(
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: Icon(Icons.barcode_reader),
|
child: Icon(Icons.barcode_reader),
|
||||||
@ -182,12 +183,29 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
|
|||||||
scanBarcode(
|
scanBarcode(
|
||||||
context,
|
context,
|
||||||
handler: POReceiveBarcodeHandler(purchaseOrder: widget.order),
|
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;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import "package:inventree/widget/refreshable_state.dart";
|
|||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/barcode/barcode.dart";
|
import "package:inventree/barcode/barcode.dart";
|
||||||
|
import "package:inventree/barcode/purchase_order.dart";
|
||||||
import "package:inventree/inventree/purchase_order.dart";
|
import "package:inventree/inventree/purchase_order.dart";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -5,6 +5,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
|||||||
|
|
||||||
import "package:inventree/app_colors.dart";
|
import "package:inventree/app_colors.dart";
|
||||||
import "package:inventree/barcode/barcode.dart";
|
import "package:inventree/barcode/barcode.dart";
|
||||||
|
import "package:inventree/barcode/purchase_order.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
|
|
||||||
import "package:inventree/inventree/stock.dart";
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user