From 730521fd00e92531fc30f854ce28817504c515c0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 5 Dec 2022 23:39:40 +1100 Subject: [PATCH] New barcode actions (#218) * Bump release notes * Adds method for linking custom barcodes * Custom getter method for determining if an item has barcode data * Add method to check if the API supports "modern" barcodes * Refactor custom barcode implementation for StockItem - Needs testing * Unit testing for linking and unlinking barcodes * Fixes * Refactor code for "custom barcode action" tile * Add custom barcode action to StockLocation * Add extra debug to debug the debugging * Unit test fix * Change scope I guess? * remove handler test --- assets/release_notes.md | 4 +- lib/api.dart | 25 ++++++++ lib/barcode.dart | 67 +++++++++++++++++++--- lib/helpers.dart | 5 +- lib/inventree/model.dart | 17 ++++++ lib/inventree/stock.dart | 2 - lib/l10n/app_en.arb | 3 + lib/widget/location_display.dart | 6 ++ lib/widget/part_detail.dart | 29 +--------- lib/widget/stock_detail.dart | 97 +------------------------------- test/barcode_test.dart | 41 +++++++++++++- 11 files changed, 162 insertions(+), 134 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index 6a494584..5294596d 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,9 +1,11 @@ ## InvenTree App Release Notes --- -### - December 2022 +### 0.9.0 - November 2022 --- +- Added support for custom barcodes for Parts +- Added support for custom barcode for Stock Locations - Support Part parameters - Add support for structural part categories - Add support for structural stock locations diff --git a/lib/api.dart b/lib/api.dart index 56d92cb7..f7d3a248 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -259,6 +259,9 @@ class InvenTreeAPI { // Notification support requires API v25 or newer bool get supportsNotifications => isConnected() && apiVersion >= 25; + // Supports 'modern' barcode API (v80 or newer) + bool get supportModernBarcodes => isConnected() && apiVersion >= 80; + // Structural categories requires API v83 or newer bool get supportsStructuralCategories => isConnected() && apiVersion >= 83; @@ -883,6 +886,27 @@ class InvenTreeAPI { ); } + /* + * Perform a request to link a custom barcode to a particular item + */ + Future linkBarcode(Map body) async { + + HttpClientRequest? request = await apiRequest("/barcode/link/", "POST"); + + if (request == null) { + return false; + } + + final response = await completeRequest( + request, + data: json.encode(body), + statusCode: 200 + ); + + return response.isValid() && response.statusCode == 200; + + } + /* * Perform a request to unlink a custom barcode from a particular item */ @@ -1255,6 +1279,7 @@ class InvenTreeAPI { ); } + // Return True if the API supports 'settings' (requires API v46) bool get supportsSettings => isConnected() && apiVersion >= 46; // Keep a record of which settings we have received from the server diff --git a/lib/barcode.dart b/lib/barcode.dart index 3b87770d..f3d8a272 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -1,21 +1,21 @@ import "dart:io"; - -import "package:inventree/inventree/sentry.dart"; -import "package:inventree/widget/dialogs.dart"; -import "package:inventree/widget/snacks.dart"; import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:one_context/one_context.dart"; - import "package:qr_code_scanner/qr_code_scanner.dart"; -import "package:inventree/inventree/stock.dart"; -import "package:inventree/inventree/part.dart"; +import "package:inventree/app_colors.dart"; import "package:inventree/api.dart"; import "package:inventree/helpers.dart"; import "package:inventree/l10.dart"; import "package:inventree/preferences.dart"; +import "package:inventree/inventree/sentry.dart"; +import "package:inventree/inventree/stock.dart"; +import "package:inventree/inventree/part.dart"; + +import "package:inventree/widget/dialogs.dart"; +import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/location_display.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/stock_detail.dart"; @@ -117,6 +117,8 @@ class BarcodeHandler { expectedStatusCode: null, // Do not show an error on "unexpected code" ); + debug("Barcode scan response" + response.data.toString()); + _controller?.resumeCamera(); Map data = response.asMap(); @@ -726,8 +728,57 @@ class _QRViewState extends State { } Future scanQrCode(BuildContext context) async { - Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler()))); return; +} + + +/* + * Construct a generic ListTile widget to link or un-link a custom barcode from a model. + */ +Widget customBarcodeActionTile(BuildContext context, String barcode, String model, int pk) { + + if (barcode.isEmpty) { + return ListTile( + title: Text(L10().barcodeAssign), + subtitle: Text(L10().barcodeAssignDetail), + leading: Icon(Icons.qr_code, color: COLOR_CLICK), + trailing: Icon(Icons.qr_code_scanner), + onTap: () { + var handler = UniqueBarcodeHandler((String barcode) { + InvenTreeAPI().linkBarcode({ + model: pk.toString(), + "barcode": barcode, + }).then((bool result) { + showSnackIcon( + result ? L10().barcodeAssigned : L10().barcodeNotAssigned, + success: result + ); + }); + }); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => InvenTreeQRView(handler) + ) + ); + } + ); + } else { + return ListTile( + title: Text(L10().barcodeUnassign), + leading: Icon(Icons.qr_code, color: COLOR_CLICK), + onTap: () async { + InvenTreeAPI().unlinkBarcode({ + model: pk.toString() + }).then((bool result) { + showSnackIcon( + result ? L10().requestSuccessful : L10().requestFailed, + ); + }); + }, + ); + } } \ No newline at end of file diff --git a/lib/helpers.dart b/lib/helpers.dart index d75e34d1..b574c609 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -17,7 +17,10 @@ List debug_messages = []; void clearDebugMessage() => debug_messages.clear(); -int debugMessageCount() => debug_messages.length; +int debugMessageCount() { + print("Debug Messages: ${debug_messages.length}"); + return debug_messages.length; +} // Check if the debug log contains a given message bool debugContains(String msg, {bool raiseAssert = true}) { diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 00605212..d0984d69 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -148,6 +148,23 @@ class InvenTreeModel { // Legacy API provided external link as "URL", while newer API uses "link" String get link => (jsondata["link"] ?? jsondata["URL"] ?? "") as String; + /* Extract any custom barcode data available for the model. + * Note that old API used 'uid' (only for StockItem), + * but this was updated to use 'barcode_hash' + */ + String get customBarcode { + if (jsondata.containsKey("uid")) { + return jsondata["uid"] as String; + } else if (jsondata.containsKey("barcode_hash")) { + return jsondata["barcode_hash"] as String; + } else if (jsondata.containsKey("barcode")) { + return jsondata["barcode"] as String; + } + + // Empty string if no match + return ""; + } + Future goToInvenTreePage() async { if (await canLaunch(webUrl)) { diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 2e9e441b..ff29cd49 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -276,8 +276,6 @@ class InvenTreeStockItem extends InvenTreeModel { }); } - String get uid => (jsondata["uid"] ?? "") as String; - int get status => (jsondata["status"] ?? -1) as int; String get packaging => (jsondata["packaging"] ?? "") as String; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4f0261b1..06223ea1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -807,6 +807,9 @@ "request": "Request", "@request": {}, + "requestFailed": "Request Failed", + "@requestFailed": {}, + "requestSuccessful": "Request successful", "@requestSuccessful": {}, diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 0eaad482..3beefb62 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -453,6 +453,12 @@ class _LocationDisplayState extends RefreshableState { ) ); } + + if (InvenTreeAPI().supportModernBarcodes) { + tiles.add( + customBarcodeActionTile(context, location!.customBarcode, "stocklocation", location!.pk) + ); + } } } diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index d5bdc593..1a542af3 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -4,6 +4,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; +import "package:inventree/barcode.dart"; import "package:inventree/l10.dart"; import "package:inventree/helpers.dart"; @@ -695,35 +696,11 @@ class _PartDisplayState extends RefreshableState { ) ); - // TODO - Add this action back in once implemented - /* - tiles.add( - ListTile( - title: Text(L10().barcodeScanItem), - leading: FaIcon(FontAwesomeIcons.box), - trailing: Icon(Icons.qr_code), - onTap: () { - // TODO - }, - ), - ); - */ - - /* - // TODO: Implement part deletion - if (!part.isActive && InvenTreeAPI().checkPermission("part", "delete")) { + if (InvenTreeAPI().supportModernBarcodes) { tiles.add( - ListTile( - title: Text(L10().deletePart), - subtitle: Text(L10().deletePartDetail), - leading: FaIcon(FontAwesomeIcons.trashAlt, color: COLOR_DANGER), - onTap: () { - // TODO - }, - ) + customBarcodeActionTile(context, part.customBarcode, "part", part.pk) ); } - */ return tiles; } diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 9376e366..dcfd87d5 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -416,48 +416,6 @@ class _StockItemDisplayState extends RefreshableState { ); } - - /* - * Unassign (remove) a barcode from a StockItem. - * - * Note that for API version < 76 this action is performed on the StockItem endpoint. - * For API version 76 or above, this uses the barcode "unlink" endpoint - */ - Future _unassignBarcode(BuildContext context) async { - - if (InvenTreeAPI().apiVersion < 76) { - final response = await item.update(values: {"uid": ""}); - - switch (response.statusCode) { - case 200: - case 201: - showSnackIcon( - L10().stockItemUpdateSuccess, - success: true - ); - break; - default: - showSnackIcon( - L10().stockItemUpdateFailure, - success: false, - ); - break; - } - } else { - final bool result = await InvenTreeAPI().unlinkBarcode({ - "stockitem": item.pk, - }); - - showSnackIcon( - result ? L10().stockItemUpdateSuccess : L10().stockItemUpdateFailure, - success: result, - ); - } - - refresh(context); - } - - /* * Launches an API Form to transfer this stock item to a new location */ @@ -844,59 +802,8 @@ class _StockItemDisplayState extends RefreshableState { ) ); - // Add or remove custom barcode - if (item.uid.isEmpty) { - tiles.add( - ListTile( - title: Text(L10().barcodeAssign), - subtitle: Text(L10().barcodeAssignDetail), - leading: Icon(Icons.qr_code), - trailing: Icon(Icons.qr_code_scanner), - onTap: () { - - var handler = UniqueBarcodeHandler((String hash) { - item.update( - values: { - "uid": hash, - } - ).then((response) { - - switch (response.statusCode) { - case 200: - case 201: - barcodeSuccessTone(); - - showSnackIcon( - L10().barcodeAssigned, - success: true, - icon: Icons.qr_code, - ); - - refresh(context); - break; - default: - break; - } - }); - }); - - Navigator.push( - context, - MaterialPageRoute(builder: (context) => InvenTreeQRView(handler)) - ); - } - ) - ); - } else { - tiles.add( - ListTile( - title: Text(L10().barcodeUnassign), - leading: Icon(Icons.qr_code, color: COLOR_CLICK), - onTap: () { - _unassignBarcode(context); - } - ) - ); + if (InvenTreeAPI().supportModernBarcodes) { + tiles.add(customBarcodeActionTile(context, item.customBarcode, "stockitem", item.pk)); } // Print label (if label printing plugins exist) diff --git a/test/barcode_test.dart b/test/barcode_test.dart index 657d1533..eaafa683 100644 --- a/test/barcode_test.dart +++ b/test/barcode_test.dart @@ -12,6 +12,7 @@ import "package:inventree/barcode.dart"; import "package:inventree/helpers.dart"; import "package:inventree/user_profile.dart"; +import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/stock.dart"; void main() { @@ -75,7 +76,7 @@ void main() { debugContains("Scanned barcode data: '{\"stocklocation\": 999999}'"); debugContains("showSnackIcon: 'No match for barcode'"); - assert(debugMessageCount() == 2); + assert(debugMessageCount() == 3); }); }); @@ -157,4 +158,42 @@ void main() { debugContains("showSnackIcon: 'Scanned into location'"); }); }); + + group("Test PartBarcodes:", () { + + // Assign a custom barcode to a Part instance + test("Assign Barcode", () async { + + // Unlink barcode first + await InvenTreeAPI().unlinkBarcode({ + "part": "2" + }); + + final part = await InvenTreePart().get(2) as InvenTreePart?; + + assert(part != null); + assert(part!.pk == 2); + + // Should have a "null" barcode + assert(part!.customBarcode.isEmpty); + + // Assign custom barcode data to the part + await InvenTreeAPI().linkBarcode({ + "part": "2", + "barcode": "xyz-123" + }); + + await part!.reload(); + assert(part.customBarcode.isNotEmpty); + + // Check we can de-register a barcode also + // Unlink barcode first + await InvenTreeAPI().unlinkBarcode({ + "part": "2" + }); + + await part.reload(); + assert(part.customBarcode.isEmpty); + }); + }); } \ No newline at end of file