mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 05:26:47 +00:00
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
This commit is contained in:
parent
efb6fc353e
commit
730521fd00
@ -1,9 +1,11 @@
|
|||||||
## InvenTree App Release Notes
|
## 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
|
- Support Part parameters
|
||||||
- Add support for structural part categories
|
- Add support for structural part categories
|
||||||
- Add support for structural stock locations
|
- Add support for structural stock locations
|
||||||
|
25
lib/api.dart
25
lib/api.dart
@ -259,6 +259,9 @@ class InvenTreeAPI {
|
|||||||
// Notification support requires API v25 or newer
|
// Notification support requires API v25 or newer
|
||||||
bool get supportsNotifications => isConnected() && apiVersion >= 25;
|
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
|
// Structural categories requires API v83 or newer
|
||||||
bool get supportsStructuralCategories => isConnected() && apiVersion >= 83;
|
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<bool> linkBarcode(Map<String, String> 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
|
* 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;
|
bool get supportsSettings => isConnected() && apiVersion >= 46;
|
||||||
|
|
||||||
// Keep a record of which settings we have received from the server
|
// Keep a record of which settings we have received from the server
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import "dart:io";
|
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:flutter/material.dart";
|
||||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import "package:one_context/one_context.dart";
|
import "package:one_context/one_context.dart";
|
||||||
|
|
||||||
import "package:qr_code_scanner/qr_code_scanner.dart";
|
import "package:qr_code_scanner/qr_code_scanner.dart";
|
||||||
|
|
||||||
import "package:inventree/inventree/stock.dart";
|
import "package:inventree/app_colors.dart";
|
||||||
import "package:inventree/inventree/part.dart";
|
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
import "package:inventree/preferences.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/location_display.dart";
|
||||||
import "package:inventree/widget/part_detail.dart";
|
import "package:inventree/widget/part_detail.dart";
|
||||||
import "package:inventree/widget/stock_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"
|
expectedStatusCode: null, // Do not show an error on "unexpected code"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
debug("Barcode scan response" + response.data.toString());
|
||||||
|
|
||||||
_controller?.resumeCamera();
|
_controller?.resumeCamera();
|
||||||
|
|
||||||
Map<String, dynamic> data = response.asMap();
|
Map<String, dynamic> data = response.asMap();
|
||||||
@ -726,8 +728,57 @@ class _QRViewState extends State<InvenTreeQRView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> scanQrCode(BuildContext context) async {
|
Future<void> scanQrCode(BuildContext context) async {
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler())));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler())));
|
||||||
|
|
||||||
return;
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,10 @@ List<String> debug_messages = [];
|
|||||||
|
|
||||||
void clearDebugMessage() => debug_messages.clear();
|
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
|
// Check if the debug log contains a given message
|
||||||
bool debugContains(String msg, {bool raiseAssert = true}) {
|
bool debugContains(String msg, {bool raiseAssert = true}) {
|
||||||
|
@ -148,6 +148,23 @@ class InvenTreeModel {
|
|||||||
// Legacy API provided external link as "URL", while newer API uses "link"
|
// Legacy API provided external link as "URL", while newer API uses "link"
|
||||||
String get link => (jsondata["link"] ?? jsondata["URL"] ?? "") as String;
|
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 <void> goToInvenTreePage() async {
|
Future <void> goToInvenTreePage() async {
|
||||||
|
|
||||||
if (await canLaunch(webUrl)) {
|
if (await canLaunch(webUrl)) {
|
||||||
|
@ -276,8 +276,6 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String get uid => (jsondata["uid"] ?? "") as String;
|
|
||||||
|
|
||||||
int get status => (jsondata["status"] ?? -1) as int;
|
int get status => (jsondata["status"] ?? -1) as int;
|
||||||
|
|
||||||
String get packaging => (jsondata["packaging"] ?? "") as String;
|
String get packaging => (jsondata["packaging"] ?? "") as String;
|
||||||
|
@ -807,6 +807,9 @@
|
|||||||
"request": "Request",
|
"request": "Request",
|
||||||
"@request": {},
|
"@request": {},
|
||||||
|
|
||||||
|
"requestFailed": "Request Failed",
|
||||||
|
"@requestFailed": {},
|
||||||
|
|
||||||
"requestSuccessful": "Request successful",
|
"requestSuccessful": "Request successful",
|
||||||
"@requestSuccessful": {},
|
"@requestSuccessful": {},
|
||||||
|
|
||||||
|
@ -453,6 +453,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (InvenTreeAPI().supportModernBarcodes) {
|
||||||
|
tiles.add(
|
||||||
|
customBarcodeActionTile(context, location!.customBarcode, "stocklocation", location!.pk)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
|||||||
|
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/app_colors.dart";
|
import "package:inventree/app_colors.dart";
|
||||||
|
import "package:inventree/barcode.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
|
|
||||||
@ -695,35 +696,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO - Add this action back in once implemented
|
if (InvenTreeAPI().supportModernBarcodes) {
|
||||||
/*
|
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
customBarcodeActionTile(context, part.customBarcode, "part", part.pk)
|
||||||
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")) {
|
|
||||||
tiles.add(
|
|
||||||
ListTile(
|
|
||||||
title: Text(L10().deletePart),
|
|
||||||
subtitle: Text(L10().deletePartDetail),
|
|
||||||
leading: FaIcon(FontAwesomeIcons.trashAlt, color: COLOR_DANGER),
|
|
||||||
onTap: () {
|
|
||||||
// TODO
|
|
||||||
},
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
@ -416,48 +416,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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<void> _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
|
* Launches an API Form to transfer this stock item to a new location
|
||||||
*/
|
*/
|
||||||
@ -844,59 +802,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add or remove custom barcode
|
if (InvenTreeAPI().supportModernBarcodes) {
|
||||||
if (item.uid.isEmpty) {
|
tiles.add(customBarcodeActionTile(context, item.customBarcode, "stockitem", item.pk));
|
||||||
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);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print label (if label printing plugins exist)
|
// Print label (if label printing plugins exist)
|
||||||
|
@ -12,6 +12,7 @@ import "package:inventree/barcode.dart";
|
|||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
import "package:inventree/user_profile.dart";
|
import "package:inventree/user_profile.dart";
|
||||||
|
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
import "package:inventree/inventree/stock.dart";
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -75,7 +76,7 @@ void main() {
|
|||||||
|
|
||||||
debugContains("Scanned barcode data: '{\"stocklocation\": 999999}'");
|
debugContains("Scanned barcode data: '{\"stocklocation\": 999999}'");
|
||||||
debugContains("showSnackIcon: 'No match for barcode'");
|
debugContains("showSnackIcon: 'No match for barcode'");
|
||||||
assert(debugMessageCount() == 2);
|
assert(debugMessageCount() == 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -157,4 +158,42 @@ void main() {
|
|||||||
debugContains("showSnackIcon: 'Scanned into location'");
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user