From 10b435f4faa329af4894f26a5740aa775ee9ccb1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 May 2022 21:32:21 +1000 Subject: [PATCH 1/7] Merge pull request #132 from inventree/search-fix Search fix (cherry picked from commit a4814816ad4772ab0c2c08e33aef7d66f8800edf) --- assets/release_notes.md | 5 ++ lib/inventree/model.dart | 2 - lib/l10n/app_en.arb | 3 + lib/widget/search.dart | 177 ++++++++++++++++++++++++++------------- 4 files changed, 126 insertions(+), 61 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index ca14a6c8..e240c26d 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,6 +1,11 @@ ## InvenTree App Release Notes --- +### 0.7.1 - May 2022 +--- + +- Fixes issue which prevented text input in search window + ### 0.7.0 - May 2022 --- diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index decb7aaa..550bc2f5 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -216,8 +216,6 @@ class InvenTreeModel { if (response.isValid()) { int n = int.tryParse(response.data["count"].toString()) ?? 0; - - print("${URL} -> ${n} results"); return n; } else { return 0; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3b13d9cf..f6e60649 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -797,6 +797,9 @@ "description": "search" }, + "searching": "Searching", + "@searching": {}, + "searchLocation": "Search for location", "@searchLocation": {}, diff --git a/lib/widget/search.dart b/lib/widget/search.dart index 2d21fc66..c2bc63f5 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -4,7 +4,6 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/widget/part_list.dart"; import "package:inventree/widget/purchase_order_list.dart"; @@ -36,6 +35,19 @@ class _SearchDisplayState extends RefreshableState { final bool hasAppBar; + @override + void initState() { + super.initState(); + + _focusNode = FocusNode(); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + @override String getAppBarTitle(BuildContext context) => L10().search; @@ -52,6 +64,17 @@ class _SearchDisplayState extends RefreshableState { Timer? debounceTimer; + bool isSearching() { + + if (searchController.text.isEmpty) { + return false; + } + + return nSearchResults < 5; + } + + int nSearchResults = 0; + int nPartResults = 0; int nCategoryResults = 0; @@ -64,6 +87,8 @@ class _SearchDisplayState extends RefreshableState { int nPurchaseOrderResults = 0; + late FocusNode _focusNode; + // Callback when the text is being edited // Incorporates a debounce timer to restrict search frequency void onSearchTextChanged(String text, {bool immediate = false}) { @@ -79,72 +104,88 @@ class _SearchDisplayState extends RefreshableState { search(text); }); } - } + /* + * Initiate multiple search requests to the server. + * Each request returns at *some point* in the future, + * by which time the search input may have changed, giving unexpected results. + * + * So, each request only causes an update *if* the search term is still the same when it completes + */ Future search(String term) async { - if (term.isEmpty) { - setState(() { - // Do not search on an empty string - nPartResults = 0; - nCategoryResults = 0; - nStockResults = 0; - nLocationResults = 0; - nSupplierResults = 0; - nPurchaseOrderResults = 0; - }); + setState(() { + // Do not search on an empty string + nPartResults = 0; + nCategoryResults = 0; + nStockResults = 0; + nLocationResults = 0; + nSupplierResults = 0; + nPurchaseOrderResults = 0; + nSearchResults = 0; + }); + + if (term.isEmpty) { return; } // Search parts - InvenTreePart().count( - searchQuery: term - ).then((int n) { - setState(() { - nPartResults = n; - }); + InvenTreePart().count(searchQuery: term).then((int n) { + if (term == searchController.text) { + setState(() { + nPartResults = n; + nSearchResults++; + }); + } }); // Search part categories - InvenTreePartCategory().count( - searchQuery: term, - ).then((int n) { - setState(() { - nCategoryResults = n; - }); + InvenTreePartCategory().count(searchQuery: term,).then((int n) { + if (term == searchController.text) { + setState(() { + nCategoryResults = n; + nSearchResults++; + }); + } }); // Search stock items - InvenTreeStockItem().count( - searchQuery: term - ).then((int n) { - setState(() { - nStockResults = n; - }); + InvenTreeStockItem().count(searchQuery: term).then((int n) { + if (term == searchController.text) { + setState(() { + nStockResults = n; + nSearchResults++; + }); + } }); // Search stock locations - InvenTreeStockLocation().count( - searchQuery: term - ).then((int n) { - setState(() { - nLocationResults = n; - }); + InvenTreeStockLocation().count(searchQuery: term).then((int n) { + if (term == searchController.text) { + setState(() { + nLocationResults = n; + + nSearchResults++; + }); + } }); + // TDOO: Re-implement this once display for companies has been fixed + /* // Search suppliers - InvenTreeCompany().count( - searchQuery: term, + InvenTreeCompany().count(searchQuery: term, filters: { "is_supplier": "true", }, ).then((int n) { setState(() { nSupplierResults = n; + nSearchResults++; }); }); + */ // Search purchase orders InvenTreePurchaseOrder().count( @@ -153,9 +194,12 @@ class _SearchDisplayState extends RefreshableState { "outstanding": "true" } ).then((int n) { - setState(() { - nPurchaseOrderResults = n; - }); + if (term == searchController.text) { + setState(() { + nPurchaseOrderResults = n; + nSearchResults++; + }); + } }); } @@ -166,29 +210,31 @@ class _SearchDisplayState extends RefreshableState { // Search input tiles.add( - InputDecorator( + TextFormField( decoration: InputDecoration( - ), - child: ListTile( - title: TextField( - readOnly: false, - decoration: InputDecoration( - helperText: L10().queryEmpty, - ), - controller: searchController, - onChanged: (String text) { - onSearchTextChanged(text); - }, + hintText: L10().queryEmpty, + prefixIcon: IconButton( + icon: FaIcon(FontAwesomeIcons.search), + onPressed: null, ), - trailing: IconButton( + suffixIcon: IconButton( icon: FaIcon(FontAwesomeIcons.backspace, color: Colors.red), onPressed: () { searchController.clear(); onSearchTextChanged("", immediate: true); - }, + _focusNode.requestFocus(); + } ), - ) - ) + ), + readOnly: false, + autofocus: true, + autocorrect: false, + focusNode: _focusNode, + controller: searchController, + onChanged: (String text) { + onSearchTextChanged(text); + }, + ), ); String query = searchController.text; @@ -335,7 +381,17 @@ class _SearchDisplayState extends RefreshableState { ); } - if (results.isEmpty && searchController.text.isNotEmpty) { + if (isSearching()) { + tiles.add( + ListTile( + title: Text(L10().searching), + leading: FaIcon(FontAwesomeIcons.search), + trailing: CircularProgressIndicator(), + ) + ); + } + + if (!isSearching() && results.isEmpty && searchController.text.isNotEmpty) { tiles.add( ListTile( title: Text(L10().queryNoResults), @@ -348,8 +404,11 @@ class _SearchDisplayState extends RefreshableState { } } - return tiles; + if (!_focusNode.hasFocus) { + _focusNode.requestFocus(); + } + return tiles; } @override From cfc9f09b809525d2ef61cc1f16e3fbf8a4b51335 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 22 May 2022 10:11:36 +1000 Subject: [PATCH 2/7] Merge pull request #135 from inventree/unit-testing Unit testing (cherry picked from commit d55f594342e07fe142fdceda76a36ee0138d270f) --- .github/workflows/android.yaml | 3 - .github/workflows/ci.yaml | 70 ++++++++++ .github/workflows/ios.yaml | 3 - .github/workflows/lint.yaml | 41 ------ .gitignore | 3 + find_dart_files.py | 46 +++++++ lib/api.dart | 33 ++--- lib/api_form.dart | 2 +- lib/app_colors.dart | 2 - lib/app_settings.dart | 68 --------- lib/barcode.dart | 69 ++++++---- lib/helpers.dart | 45 +++--- lib/inventree/sentry.dart | 2 +- lib/inventree/stock.dart | 4 +- lib/l10.dart | 14 +- lib/preferences.dart | 81 +++++++---- lib/settings/app_settings.dart | 2 +- lib/settings/home_settings.dart | 2 +- lib/settings/login.dart | 1 - lib/user_profile.dart | 121 ++++++++++++----- lib/widget/dialogs.dart | 13 +- lib/widget/drawer.dart | 4 +- lib/widget/home.dart | 18 +-- lib/widget/part_list.dart | 2 +- lib/widget/purchase_order_detail.dart | 2 +- lib/widget/snacks.dart | 18 ++- lib/widget/stock_detail.dart | 13 +- lib/widget/stock_list.dart | 2 +- pubspec.lock | 189 ++++++++++++++++++++++++++ pubspec.yaml | 31 +---- requirements.txt | 4 + test/api_test.dart | 141 +++++++++++++++++++ test/preferences_test.dart | 30 ++++ test/user_profile_test.dart | 125 +++++++++++++++++ test/widget_test.dart | 6 +- 35 files changed, 893 insertions(+), 317 deletions(-) create mode 100644 .github/workflows/ci.yaml delete mode 100644 .github/workflows/lint.yaml create mode 100644 find_dart_files.py delete mode 100644 lib/app_settings.dart create mode 100644 requirements.txt create mode 100644 test/api_test.dart create mode 100644 test/preferences_test.dart create mode 100644 test/user_profile_test.dart diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index a9d49403..4199cf28 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -6,9 +6,6 @@ on: push: branches: - master - pull_request: - branches: - - master jobs: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..f3ae7c75 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INVENTREE_DB_ENGINE: django.db.backends.sqlite3 + INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3 + INVENTREE_MEDIA_ROOT: ../test_inventree_media + INVENTREE_STATIC_ROOT: ../test_inventree_static + INVENTREE_ADMIN_USER: testuser + INVENTREE_ADMIN_PASSWORD: testpassword + INVENTREE_ADMIN_EMAIL: test@test.com + INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345 + INVENTREE_PYTHON_TEST_USERNAME: testuser + INVENTREE_PYTHON_TEST_PASSWORD: testpassword +jobs: + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + submodules: recursive + - name: Setup Java + uses: actions/setup-java@v1 + with: + java-version: '12.x' + - name: Setup Flutter + uses: subosito/flutter-action@v1 + with: + flutter-version: '2.10.3' + - name: Collect Translation Files + run: | + cd lib/l10n + python3 collect_translations.py + - name: Static Analysis Tests + run: | + cp lib/dummy_dsn.dart lib/dsn.dart + python3 find_dart_files.py + flutter pub get + flutter analyze + + - name: Start InvenTree Server + run: | + sudo apt-get install python3-dev python3-pip python3-venv python3-wheel g++ + pip3 install invoke + git clone --depth 1 https://github.com/inventree/inventree ./inventree_server + cd inventree_server + invoke install + invoke migrate + invoke import-fixtures + invoke server -a 127.0.0.1:12345 & + invoke wait + - name: Unit Tests + run: | + flutter test --coverage + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index 039a6e9a..3dc6338c 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -6,9 +6,6 @@ on: push: branches: - master - pull_request: - branches: - - master jobs: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index 44b1ed3b..00000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Run flutter linting checks - -name: lint - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - - lint: - runs-on: ubuntu-latest - - env: - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - submodules: recursive - - name: Setup Java - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - name: Setup Flutter - uses: subosito/flutter-action@v1 - with: - flutter-version: '2.10.3' - - name: Collect Translation Files - run: | - cd lib/l10n - python3 collect_translations.py - - run: flutter pub get - - run: cp lib/dummy_dsn.dart lib/dsn.dart - - run: flutter analyze - - run: flutter test --coverage diff --git a/.gitignore b/.gitignore index 2f258989..07101c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ coverage/* +# This file is auto-generated as part of the CI process +test/coverage_helper_test.dart + # Sentry API key lib/dsn.dart diff --git a/find_dart_files.py b/find_dart_files.py new file mode 100644 index 00000000..a182daaa --- /dev/null +++ b/find_dart_files.py @@ -0,0 +1,46 @@ +""" +This script recursively finds any '.dart' files in the ./lib directory, +and generates a 'test' file which includes all these files. + +This is to ensure that *all* .dart files are included in test coverage. +By default, source files which are not touched by the unit tests are not included! + +Ref: https://github.com/flutter/flutter/issues/27997 +""" + +from pathlib import Path + +if __name__ == '__main__': + + dart_files = Path('lib').rglob('*.dart') + + with open("test/coverage_helper_test.dart", "w") as f: + + f.write("// ignore_for_file: unused_import\n\n") + + skips = [ + 'generated', + 'l10n', + 'dsn.dart', + ] + + for path in dart_files: + path = str(path) + + if any([s in path for s in skips]): + continue + + # Remove leading 'lib\' text + path = path[4:] + path = path.replace('\\', '/') + f.write(f'import "package:inventree/{path}";\n') + + f.write("\n\n") + + f.write("// DO NOT EDIT THIS FILE - it has been auto-generated by 'find_dart_files.py'\n") + f.write("// It has been created to ensure that *all* source file are included in coverage data\n") + + f.write('import "package:test/test.dart";\n\n'); + + f.write("// Do not actually test anything!\n") + f.write("void main() {}\n") diff --git a/lib/api.dart b/lib/api.dart index 9994b655..52bd29a1 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -6,7 +6,7 @@ import "package:flutter/foundation.dart"; import "package:http/http.dart" as http; import "package:intl/intl.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:open_file/open_file.dart"; import "package:cached_network_image/cached_network_image.dart"; @@ -16,6 +16,7 @@ import "package:flutter_cache_manager/flutter_cache_manager.dart"; import "package:inventree/widget/dialogs.dart"; import "package:inventree/l10.dart"; +import "package:inventree/helpers.dart"; import "package:inventree/inventree/sentry.dart"; import "package:inventree/inventree/model.dart"; import "package:inventree/user_profile.dart"; @@ -201,11 +202,13 @@ class InvenTreeAPI { // Authentication token (initially empty, must be requested) String _token = ""; + bool get hasToken => _token.isNotEmpty; + /* * Check server connection and display messages if not connected. * Useful as a precursor check before performing operations. */ - bool checkConnection(BuildContext context) { + bool checkConnection() { // Firstly, is the server connected? if (!isConnected()) { @@ -278,7 +281,7 @@ class InvenTreeAPI { bool _connecting = false; bool isConnected() { - return profile != null && _connected && baseUrl.isNotEmpty && _token.isNotEmpty; + return profile != null && _connected && baseUrl.isNotEmpty && hasToken; } bool isConnecting() { @@ -289,14 +292,10 @@ class InvenTreeAPI { static final InvenTreeAPI _api = InvenTreeAPI._internal(); // API endpoint for receiving purchase order line items was introduced in v12 - bool supportPoReceive() { - return apiVersion >= 12; - } + bool get supportsPoReceive => apiVersion >= 12; // "Modern" API transactions were implemented in API v14 - bool supportModernStockTransactions() { - return apiVersion >= 14; - } + bool get supportsModernStockTransactions => apiVersion >= 14; /* * Connect to the remote InvenTree server: @@ -338,7 +337,7 @@ class InvenTreeAPI { // Clear the list of available plugins _plugins.clear(); - print("Connecting to ${apiUrl} -> username=${username}"); + debug("Connecting to ${apiUrl} -> username=${username}"); APIResponse response; @@ -431,7 +430,7 @@ class InvenTreeAPI { // Return the received token _token = (data["token"] ?? "") as String; - print("Received token - $_token"); + debug("Received token from server"); // Request user role information (async) getUserRoles(); @@ -445,7 +444,7 @@ class InvenTreeAPI { } void disconnectFromServer() { - print("InvenTreeAPI().disconnectFromServer()"); + debug("API : disconnectFromServer()"); _connected = false; _connecting = false; @@ -501,7 +500,7 @@ class InvenTreeAPI { roles.clear(); - print("Requesting user role data"); + debug("API: Requesting user role data"); // Next we request the permissions assigned to the current user // Note: 2021-02-27 this "roles" feature for the API was just introduced. @@ -531,7 +530,7 @@ class InvenTreeAPI { return; } - print("Requesting plugin information"); + debug("API: getPluginInformation()"); // Request a list of plugins from the server final List results = await InvenTreePlugin().list(); @@ -661,7 +660,7 @@ class InvenTreeAPI { _request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale()); } on SocketException catch (error) { - print("SocketException at ${url}: ${error.toString()}"); + debug("SocketException at ${url}: ${error.toString()}"); showServerError(L10().connectionRefused, error.toString()); return; } on TimeoutException { @@ -670,7 +669,7 @@ class InvenTreeAPI { return; } on HandshakeException catch (error) { print("HandshakeException at ${url}:"); - print(error.toString()); + debug(error.toString()); showServerError(L10().serverCertificateError, error.toString()); return; } catch (error, stackTrace) { @@ -1233,8 +1232,6 @@ class InvenTreeAPI { var plugins = getPlugins(mixin: "locate"); - print("locateItemOrLocation"); - if (plugins.isEmpty) { // TODO: Error message return; diff --git a/lib/api_form.dart b/lib/api_form.dart index 07221a1e..588658bc 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -335,7 +335,7 @@ class APIFormField { controller.text = hash; data["value"] = hash; - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeAssigned, diff --git a/lib/app_colors.dart b/lib/app_colors.dart index 99d81384..2f0395f6 100644 --- a/lib/app_colors.dart +++ b/lib/app_colors.dart @@ -1,5 +1,3 @@ - - import "dart:ui"; const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1); diff --git a/lib/app_settings.dart b/lib/app_settings.dart deleted file mode 100644 index e009b784..00000000 --- a/lib/app_settings.dart +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Class for managing app-level configuration options - */ - -import "package:sembast/sembast.dart"; -import "package:inventree/preferences.dart"; - -// Settings key values -const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed"; -const String INV_HOME_SHOW_PO = "homeShowPo"; -const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers"; -const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers"; -const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers"; - -const String INV_SOUNDS_BARCODE = "barcodeSounds"; -const String INV_SOUNDS_SERVER = "serverSounds"; - -const String INV_PART_SUBCATEGORY = "partSubcategory"; - -const String INV_STOCK_SUBLOCATION = "stockSublocation"; -const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; - -const String INV_REPORT_ERRORS = "reportErrors"; - -const String INV_STRICT_HTTPS = "strictHttps"; - -class InvenTreeSettingsManager { - - factory InvenTreeSettingsManager() { - return _manager; - } - - InvenTreeSettingsManager._internal(); - - final store = StoreRef("settings"); - - Future get _db async => InvenTreePreferencesDB.instance.database; - - Future getValue(String key, dynamic backup) async { - - final value = await store.record(key).get(await _db); - - if (value == null) { - return backup; - } - - return value; - } - - // Load a boolean setting - Future getBool(String key, bool backup) async { - final dynamic value = await getValue(key, backup); - - if (value is bool) { - return value; - } else { - return backup; - } - } - - Future setValue(String key, dynamic value) async { - - await store.record(key).put(await _db, value); - } - - // Ensure we only ever create a single instance of this class - static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal(); -} diff --git a/lib/barcode.dart b/lib/barcode.dart index 8781f2d1..85702e0e 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -11,15 +11,38 @@ import "package:qr_code_scanner/qr_code_scanner.dart"; import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/part.dart"; -import "package:inventree/l10.dart"; -import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; +import "package:inventree/helpers.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/widget/location_display.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/stock_detail.dart"; +/* + * Play an audible 'success' alert to the user. + */ +Future barcodeSuccessTone() async { + + final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; + + if (en) { + playAudioFile("sounds/barcode_scan.mp3"); + } +} + +Future barcodeFailureTone() async { + + final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; + + if (en) { + playAudioFile("sounds/barcode_error.mp3"); + } +} + + class BarcodeHandler { /* * Class which "handles" a barcode, by communicating with the InvenTree server, @@ -44,7 +67,7 @@ class BarcodeHandler { // Called when the server does not know about a barcode // Override this function - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeNoMatch, @@ -55,7 +78,7 @@ class BarcodeHandler { Future onBarcodeUnhandled(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); // Called when the server returns an unhandled response showServerError(L10().responseUnknown, data.toString()); @@ -125,7 +148,7 @@ class BarcodeScanHandler extends BarcodeHandler { @override Future onBarcodeUnknown(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeNoMatch, @@ -146,7 +169,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreeStockLocation().get(pk).then((var loc) { if (loc is InvenTreeStockLocation) { @@ -156,7 +179,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockLocation, @@ -170,7 +193,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreeStockItem().get(pk).then((var item) { @@ -183,7 +206,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockItem, @@ -196,7 +219,7 @@ class BarcodeScanHandler extends BarcodeHandler { if (pk > 0) { - successTone(); + barcodeSuccessTone(); InvenTreePart().get(pk).then((var part) { @@ -209,7 +232,7 @@ class BarcodeScanHandler extends BarcodeHandler { }); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidPart, @@ -218,7 +241,7 @@ class BarcodeScanHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeUnknown, @@ -275,7 +298,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { if (result) { - successTone(); + barcodeSuccessTone(); Navigator.of(context).pop(); @@ -285,7 +308,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { ); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeScanIntoLocationFailure, @@ -294,7 +317,7 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockLocation, @@ -329,14 +352,14 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { if (item == null) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().invalidStockItem, success: false, ); } else if (item.locationId == location.pk) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().itemInLocation, @@ -347,7 +370,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { if (result) { - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeScanIntoLocationSuccess, @@ -355,7 +378,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { ); } else { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeScanIntoLocationFailure, @@ -365,7 +388,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { } } else { - failureTone(); + barcodeFailureTone(); // Does not match a valid stock item! showSnackIcon( @@ -401,7 +424,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { @override Future onBarcodeMatched(BuildContext context, Map data) async { - failureTone(); + barcodeFailureTone(); // If the barcode is known, we can"t assign it to the stock item! showSnackIcon( @@ -424,7 +447,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { String hash = (data["hash"] ?? "") as String; if (hash.isEmpty) { - failureTone(); + barcodeFailureTone(); showSnackIcon( L10().barcodeError, @@ -432,7 +455,7 @@ class UniqueBarcodeHandler extends BarcodeHandler { ); } else { - successTone(); + barcodeSuccessTone(); // Close the barcode scanner Navigator.of(context).pop(); diff --git a/lib/helpers.dart b/lib/helpers.dart index 524cd92a..24dc6e84 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -7,8 +7,22 @@ * supressing trailing zeroes */ +import "dart:io"; + import "package:audioplayers/audioplayers.dart"; -import "package:inventree/app_settings.dart"; +import "package:one_context/one_context.dart"; + + +/* + * Display a debug message if we are in testing mode, or running in debug mode + */ +void debug(dynamic msg) { + + if (Platform.environment.containsKey("FLUTTER_TEST")) { + print("DEBUG: ${msg.toString()}"); + } +} + String simpleNumberString(double number) { // Ref: https://stackoverflow.com/questions/55152175/how-to-remove-trailing-zeros-using-dart @@ -16,22 +30,19 @@ String simpleNumberString(double number) { return number.toStringAsFixed(number.truncateToDouble() == number ? 0 : 1); } -Future successTone() async { +/* + * Play an audio file from the requested path. + * + * Note: If OneContext module fails the 'hasContext' check, + * we will not attempt to play the sound + */ +Future playAudioFile(String path) async { - final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; - - if (en) { - final player = AudioCache(); - player.play("sounds/barcode_scan.mp3"); + if (!OneContext.hasContext) { + return; } + + final player = AudioCache(); + player.play(path); + } - -Future failureTone() async { - - final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; - - if (en) { - final player = AudioCache(); - player.play("sounds/barcode_error.mp3"); - } -} \ No newline at end of file diff --git a/lib/inventree/sentry.dart b/lib/inventree/sentry.dart index 429a50af..cd05a14f 100644 --- a/lib/inventree/sentry.dart +++ b/lib/inventree/sentry.dart @@ -1,7 +1,7 @@ import "dart:io"; import "package:device_info_plus/device_info_plus.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:package_info_plus/package_info_plus.dart"; import "package:sentry_flutter/sentry_flutter.dart"; diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 3dc4f2b1..2bb741f2 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -533,7 +533,7 @@ class InvenTreeStockItem extends InvenTreeModel { Map data = {}; // Note: Format of adjustment API was updated in API v14 - if (api.supportModernStockTransactions()) { + if (api.supportsModernStockTransactions) { // Modern (> 14) API data = { "items": [ @@ -560,7 +560,7 @@ class InvenTreeStockItem extends InvenTreeModel { } // Expected API return code depends on server API version - final int expected_response = api.supportModernStockTransactions() ? 201 : 200; + final int expected_response = api.supportsModernStockTransactions ? 201 : 200; var response = await api.post( endpoint, diff --git a/lib/l10.dart b/lib/l10.dart index ce9f3199..2b44f962 100644 --- a/lib/l10.dart +++ b/lib/l10.dart @@ -7,16 +7,18 @@ import "package:flutter/material.dart"; // Shortcut function to reduce boilerplate! I18N L10() { - BuildContext? _ctx = OneContext().context; + if (OneContext.hasContext) { + BuildContext? _ctx = OneContext().context; - if (_ctx != null) { - I18N? i18n = I18N.of(_ctx); + if (_ctx != null) { + I18N? i18n = I18N.of(_ctx); - if (i18n != null) { - return i18n; + if (i18n != null) { + return i18n; + } } } // Fallback for "null" context - return I18NEn(); + return I18NEn(); } \ No newline at end of file diff --git a/lib/preferences.dart b/lib/preferences.dart index 2c52a88d..b804cbd9 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -6,6 +6,26 @@ import "package:sembast/sembast_io.dart"; import "package:path/path.dart"; +// Settings key values +const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed"; +const String INV_HOME_SHOW_PO = "homeShowPo"; +const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers"; +const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers"; +const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers"; + +const String INV_SOUNDS_BARCODE = "barcodeSounds"; +const String INV_SOUNDS_SERVER = "serverSounds"; + +const String INV_PART_SUBCATEGORY = "partSubcategory"; + +const String INV_STOCK_SUBLOCATION = "stockSublocation"; +const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; + +const String INV_REPORT_ERRORS = "reportErrors"; + +const String INV_STRICT_HTTPS = "strictHttps"; + + /* * Class for storing InvenTree preferences in a NoSql DB */ @@ -40,46 +60,59 @@ class InvenTreePreferencesDB { // Get a platform-specific directory where persistent app data can be stored final appDocumentDir = await getApplicationDocumentsDirectory(); - print("Documents Dir: ${appDocumentDir.toString()}"); - - print("Path: ${appDocumentDir.path}"); - // Path with the form: /platform-specific-directory/demo.db final dbPath = join(appDocumentDir.path, "InvenTreeSettings.db"); final database = await databaseFactoryIo.openDatabase(dbPath); - // Any code awaiting the Completer's future will now start executing _dbOpenCompleter.complete(database); } } -class InvenTreePreferences { - factory InvenTreePreferences() { - return _api; +/* + * InvenTree setings manager class. + * Provides functions for loading and saving settings, with provision for default values + */ +class InvenTreeSettingsManager { + + factory InvenTreeSettingsManager() { + return _manager; } - InvenTreePreferences._internal(); + InvenTreeSettingsManager._internal(); - /* The following settings are not stored to persistent storage, - * instead they are only used as "session preferences". - * They are kept here as a convenience only. - */ + final store = StoreRef("settings"); - // Expand subcategory list in PartCategory view - bool expandCategoryList = false; + Future get _db async => InvenTreePreferencesDB.instance.database; - // Expand part list in PartCategory view - bool expandPartList = true; + Future getValue(String key, dynamic backup) async { - // Expand sublocation list in StockLocation view - bool expandLocationList = false; + final value = await store.record(key).get(await _db); - // Expand item list in StockLocation view - bool expandStockList = true; + if (value == null) { + return backup; + } - // Ensure we only ever create a single instance of the preferences class - static final InvenTreePreferences _api = InvenTreePreferences._internal(); + return value; + } -} \ No newline at end of file + // Load a boolean setting + Future getBool(String key, bool backup) async { + final dynamic value = await getValue(key, backup); + + if (value is bool) { + return value; + } else { + return backup; + } + } + + Future setValue(String key, dynamic value) async { + + await store.record(key).put(await _db, value); + } + + // Ensure we only ever create a single instance of this class + static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal(); +} diff --git a/lib/settings/app_settings.dart b/lib/settings/app_settings.dart index 124246d1..f322bf7c 100644 --- a/lib/settings/app_settings.dart +++ b/lib/settings/app_settings.dart @@ -3,7 +3,7 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/l10.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class InvenTreeAppSettingsWidget extends StatefulWidget { diff --git a/lib/settings/home_settings.dart b/lib/settings/home_settings.dart index c5776c65..13e19cd2 100644 --- a/lib/settings/home_settings.dart +++ b/lib/settings/home_settings.dart @@ -5,7 +5,7 @@ import "package:inventree/l10.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class HomeScreenSettingsWidget extends StatefulWidget { @override diff --git a/lib/settings/login.dart b/lib/settings/login.dart index a998af41..e61c08b4 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -344,7 +344,6 @@ class _ProfileEditState extends State { Uri uri = Uri.parse(value); if (uri.hasScheme) { - print("Scheme: ${uri.scheme}"); if (!["http", "https"].contains(uri.scheme.toLowerCase())) { return L10().serverStart; } diff --git a/lib/user_profile.dart b/lib/user_profile.dart index 251aa309..9eff1edf 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -1,6 +1,7 @@ import "package:sembast/sembast.dart"; +import "package:inventree/helpers.dart"; import "package:inventree/preferences.dart"; class UserProfile { @@ -62,75 +63,94 @@ class UserProfileDBManager { Future get _db async => InvenTreePreferencesDB.instance.database; + /* + * Check if a profile with the specified name exists in the database + */ Future profileNameExists(String name) async { - final finder = Finder(filter: Filter.equals("name", name)); + final profiles = await getAllProfiles(); - final profiles = await store.find(await _db, finder: finder); + for (var prf in profiles) { + if (name == prf.name) { + return true; + } + } - return profiles.isNotEmpty; + // No match found! + return false; } - Future addProfile(UserProfile profile) async { + /* + * Add a new UserProfile to the profiles database. + */ + Future addProfile(UserProfile profile) async { + + if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { + debug("addProfile() : Profile missing required values - not adding to database"); + return false; + } // Check if a profile already exists with the name final bool exists = await profileNameExists(profile.name); if (exists) { - print("UserProfile '${profile.name}' already exists"); - return; + debug("addProfile() : UserProfile '${profile.name}' already exists"); + return false; } int key = await store.add(await _db, profile.toJson()) as int; - print("Added user profile <${key}> - '${profile.name}'"); - // Record the key profile.key = key; + + return true; } - Future selectProfile(int key) async { - /* - * Mark the particular profile as selected - */ + /* + * Update the selected profile in the database. + * The unique integer is used to determine if the profile already exists. + */ + Future updateProfile(UserProfile profile) async { - final result = await store.record("selected").put(await _db, key); - - return result; - } - - Future updateProfile(UserProfile profile) async { - - if (profile.key == null) { - await addProfile(profile); - return; + // Prevent invalid profile data from being updated + if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { + debug("updateProfile() : Profile missing required values - not updating"); + return false; } - final result = await store.record(profile.key).update(await _db, profile.toJson()); + if (profile.key == null) { + bool result = await addProfile(profile); + return result; + } - print("Updated user profile <${profile.key}> - '${profile.name}'"); + await store.record(profile.key).update(await _db, profile.toJson()); - return result; + return true; } + /* + * Remove a user profile from the database + */ Future deleteProfile(UserProfile profile) async { await store.record(profile.key).delete(await _db); - print("Deleted user profile <${profile.key}> - '${profile.name}'"); } + /* + * Return the currently selected profile. + * The key of the UserProfile should match the "selected" property + */ Future getSelectedProfile() async { - /* - * Return the currently selected profile. - * - * key should match the "selected" property - */ final selected = await store.record("selected").get(await _db); final profiles = await store.find(await _db); + debug("getSelectedProfile() : ${profiles.length} profiles available - selected = ${selected}"); + for (int idx = 0; idx < profiles.length; idx++) { + debug("- Checking ${idx} - key = ${profiles[idx].key} - ${profiles[idx].value.toString()}"); + if (profiles[idx].key is int && profiles[idx].key == selected) { return UserProfile.fromJson( profiles[idx].key as int, @@ -158,14 +178,43 @@ class UserProfileDBManager { if (profiles[idx].key is int) { profileList.add( - UserProfile.fromJson( - profiles[idx].key as int, - profiles[idx].value as Map, - profiles[idx].key == selected, - )); + UserProfile.fromJson( + profiles[idx].key as int, + profiles[idx].value as Map, + profiles[idx].key == selected, + ) + ); } } return profileList; } + + /* + * Mark the particular profile as selected + */ + Future selectProfile(int key) async { + await store.record("selected").put(await _db, key); + } + + /* + * Look-up and select a profile by name. + * Return true if the profile was selected + */ + Future selectProfileByName(String name) async { + var profiles = await getAllProfiles(); + + for (var prf in profiles) { + if (prf.name == name) { + int key = prf.key ?? -1; + + if (key >= 0) { + await selectProfile(key); + return true; + } + } + } + + return false; + } } diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 357983b2..5fbe20b1 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -1,12 +1,12 @@ - -import "package:inventree/app_settings.dart"; -import "package:inventree/widget/snacks.dart"; -import "package:audioplayers/audioplayers.dart"; import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/l10.dart"; +import "package:inventree/helpers.dart"; import "package:one_context/one_context.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; +import "package:inventree/widget/snacks.dart"; + Future confirmationDialog(String title, String text, {IconData icon = FontAwesomeIcons.questionCircle, String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async { String _accept = acceptText ?? L10().ok; @@ -108,8 +108,7 @@ Future showServerError(String title, String description) async { final bool tones = await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true) as bool; if (tones) { - final player = AudioCache(); - player.play("sounds/server_error.mp3"); + playAudioFile("sounds/server_error.mp3"); } showSnackIcon( diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index 85a30259..cff24de8 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -34,7 +34,7 @@ class InvenTreeDrawer extends StatelessWidget { void _search() { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; _closeDrawer(); @@ -51,7 +51,7 @@ class InvenTreeDrawer extends StatelessWidget { * Upon successful scan, data are passed off to be decoded. */ Future _scan() async { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; _closeDrawer(); scanQrCode(context); diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 64c566de..19f1c953 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -6,7 +6,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/barcode.dart"; import "package:inventree/l10.dart"; import "package:inventree/settings/login.dart"; @@ -71,13 +71,13 @@ class _InvenTreeHomePageState extends State { UserProfile? _profile; void _scan(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; scanQrCode(context); } void _showParts(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } @@ -87,7 +87,7 @@ class _InvenTreeHomePageState extends State { } void _showStarredParts(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push( context, @@ -100,13 +100,13 @@ class _InvenTreeHomePageState extends State { } void _showStock(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } void _showPurchaseOrders(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push( context, @@ -118,19 +118,19 @@ class _InvenTreeHomePageState extends State { /* void _showSuppliers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"}))); } void _showManufacturers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"}))); } void _showCustomers(BuildContext context) { - if (!InvenTreeAPI().checkConnection(context)) return; + if (!InvenTreeAPI().checkConnection()) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"}))); } diff --git a/lib/widget/part_list.dart b/lib/widget/part_list.dart index ca4d158e..528273f7 100644 --- a/lib/widget/part_list.dart +++ b/lib/widget/part_list.dart @@ -6,7 +6,7 @@ import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/api.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/l10.dart"; diff --git a/lib/widget/purchase_order_detail.dart b/lib/widget/purchase_order_detail.dart index 27486969..2abe56c6 100644 --- a/lib/widget/purchase_order_detail.dart +++ b/lib/widget/purchase_order_detail.dart @@ -247,7 +247,7 @@ class _PurchaseOrderDetailState extends RefreshableState | - */ - 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"; +/* + * Display a configurable 'snackbar' at the bottom of the screen + */ void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) { + // Escape quickly if we do not have context + if (!OneContext.hasContext) { + return; + } + BuildContext? context = OneContext().context; if (context != null) { diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index b0b3cdab..e4773755 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -19,10 +19,9 @@ import "package:inventree/widget/stock_item_history.dart"; import "package:inventree/widget/stock_item_test_results.dart"; import "package:inventree/widget/stock_notes.dart"; import "package:inventree/l10.dart"; -import "package:inventree/helpers.dart"; import "package:inventree/api.dart"; import "package:inventree/api_form.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; class StockDetailWidget extends StatefulWidget { @@ -312,7 +311,7 @@ class _StockItemDisplayState extends RefreshableState { Future _addStockDialog() async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { @@ -392,7 +391,7 @@ class _StockItemDisplayState extends RefreshableState { void _removeStockDialog() { // TODO: In future, deprecate support for the older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { "parent": "items", @@ -464,7 +463,7 @@ class _StockItemDisplayState extends RefreshableState { Future _countStockDialog() async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { @@ -567,7 +566,7 @@ class _StockItemDisplayState extends RefreshableState { Future _transferStockDialog(BuildContext context) async { // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportModernStockTransactions()) { + if (InvenTreeAPI().supportsModernStockTransactions) { Map fields = { "pk": { @@ -1008,7 +1007,7 @@ class _StockItemDisplayState extends RefreshableState { } ).then((result) { if (result) { - successTone(); + barcodeSuccessTone(); showSnackIcon( L10().barcodeAssigned, diff --git a/lib/widget/stock_list.dart b/lib/widget/stock_list.dart index 09d0dc3a..90481c78 100644 --- a/lib/widget/stock_list.dart +++ b/lib/widget/stock_list.dart @@ -5,7 +5,7 @@ import "package:inventree/inventree/stock.dart"; import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/l10.dart"; -import "package:inventree/app_settings.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/widget/stock_detail.dart"; import "package:inventree/api.dart"; diff --git a/pubspec.lock b/pubspec.lock index 4fb3ba92..0f7c12f7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "31.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.0" archive: dependency: transitive description: @@ -99,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -113,6 +134,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" cross_file: dependency: transitive description: @@ -280,6 +315,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "9.2.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" http: dependency: "direct main" description: @@ -287,6 +336,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" http_parser: dependency: transitive description: @@ -336,6 +392,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" js: dependency: transitive description: @@ -350,6 +413,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" markdown: dependency: transitive description: @@ -378,6 +448,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" octo_image: dependency: transitive description: @@ -399,6 +483,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" package_info_plus: dependency: "direct main" description: @@ -525,6 +616,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" process: dependency: transitive description: @@ -532,6 +630,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" qr_code_scanner: dependency: "direct main" description: @@ -630,6 +735,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" sky_engine: dependency: transitive description: flutter @@ -642,6 +775,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.5" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10" source_span: dependency: transitive description: @@ -705,6 +852,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.19.5" test_api: dependency: transitive description: @@ -712,6 +866,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.8" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" typed_data: dependency: transitive description: @@ -789,6 +950,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "7.5.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9031ea5d..33932d52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,10 +38,11 @@ dependencies: url_launcher: ^6.0.9 # Open link in system browser dev_dependencies: - flutter_launcher_icons: + flutter_launcher_icons: ^0.9.0 flutter_test: sdk: flutter - lint: ^1.0.0 + lint: ^1.8.0 + test: ^1.19.0 flutter_icons: android: true @@ -64,29 +65,3 @@ flutter: - assets/sounds/barcode_scan.mp3 - assets/sounds/barcode_error.mp3 - assets/sounds/server_error.mp3 - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..94f990c7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Python requirements for devops + +coverage==5.3 # Unit test coverage +coveralls==2.1.2 # Coveralls linking (for code coverage reporting) \ No newline at end of file diff --git a/test/api_test.dart b/test/api_test.dart new file mode 100644 index 00000000..b46cab64 --- /dev/null +++ b/test/api_test.dart @@ -0,0 +1,141 @@ +/* + * Unit tests for the InvenTree API code + */ + +import "package:test/test.dart"; + +import "package:inventree/api.dart"; +import "package:inventree/user_profile.dart"; + + + +void main() { + + setUp(() async { + + if (! await UserProfileDBManager().profileNameExists("Test Profile")) { + // Create and select a profile to user + + print("TEST: Creating profile for user 'testuser'"); + + await UserProfileDBManager().addProfile(UserProfile( + name: "Test Profile", + server: "http://localhost:12345", + username: "testuser", + password: "testpassword", + selected: true, + )); + } + + var prf = await UserProfileDBManager().getSelectedProfile(); + + // Ensure that the server settings are correct by default, + // as they can get overwritten by subsequent tests + + if (prf != null) { + prf.name = "Test Profile"; + prf.server = "http://localhost:12345"; + prf.username = "testuser"; + prf.password = "testpassword"; + + await UserProfileDBManager().updateProfile(prf); + } + + // Ensure the profile is selected + assert(! await UserProfileDBManager().selectProfileByName("Missing Profile")); + assert(await UserProfileDBManager().selectProfileByName("Test Profile")); + + }); + + group("Login Tests:", () { + + test("Disconnected", () async { + // Test that calling disconnect() does the right thing + var api = InvenTreeAPI(); + + api.disconnectFromServer(); + + // Check expected values + expect(api.isConnected(), equals(false)); + expect(api.isConnecting(), equals(false)); + expect(api.hasToken, equals(false)); + + }); + + test("Login Failure", () async { + // Tests for various types of login failures + var api = InvenTreeAPI(); + + // Incorrect server address + var profile = await UserProfileDBManager().getSelectedProfile(); + + assert(profile != null); + + if (profile != null) { + profile.server = "http://localhost:5555"; + await UserProfileDBManager().updateProfile(profile); + + bool result = await api.connectToServer(); + assert(!result); + + // TODO: Test the the right 'error message' is returned + // TODO: The request above should throw a 'SockeException' + + // Test incorrect login details + profile.server = "http://localhost:12345"; + profile.username = "invalidusername"; + + await UserProfileDBManager().updateProfile(profile); + + await api.connectToServer(); + assert(!result); + + // TODO: Test that the connection attempt above throws an authentication error + + assert(!api.checkConnection()); + + } else { + assert(false); + } + + }); + + test("Login Success", () async { + // Test that we can login to the server successfully + var api = InvenTreeAPI(); + + // Attempt to connect + final bool result = await api.connectToServer(); + + // Check expected values + assert(result); + assert(api.hasToken); + expect(api.baseUrl, equals("http://localhost:12345/")); + + assert(api.isConnected()); + assert(!api.isConnecting()); + assert(api.checkConnection()); + }); + + test("Version Checks", () async { + // Test server version information + var api = InvenTreeAPI(); + + assert(await api.connectToServer()); + + // Check supported functions + assert(api.apiVersion >= 50); + assert(api.supportsSettings); + assert(api.supportsNotifications); + assert(api.supportsModernStockTransactions); + assert(api.supportsPoReceive); + + // Check available permissions + assert(api.checkPermission("part", "change")); + assert(api.checkPermission("stocklocation", "delete")); + assert(api.checkPermission("part", "weirdpermission")); + assert(api.checkPermission("blah", "bloo")); + }); + + }); +} \ No newline at end of file diff --git a/test/preferences_test.dart b/test/preferences_test.dart new file mode 100644 index 00000000..b402b7a9 --- /dev/null +++ b/test/preferences_test.dart @@ -0,0 +1,30 @@ +/* + * Unit tests for the preferences manager + */ + +import "package:test/test.dart"; +import "package:inventree/preferences.dart"; + +void main() { + + setUp(() async { + + }); + + group("Settings Tests:", () { + test("Default Values", () async { + // Boolean values + expect(await InvenTreeSettingsManager().getBool("test", false), equals(false)); + expect(await InvenTreeSettingsManager().getBool("test", true), equals(true)); + + // String values + expect(await InvenTreeSettingsManager().getValue("test", "x"), equals("x")); + }); + + test("Set value", () async { + await InvenTreeSettingsManager().setValue("abc", "xyz"); + + expect(await InvenTreeSettingsManager().getValue("abc", "123"), equals("xyz")); + }); + }); +} \ No newline at end of file diff --git a/test/user_profile_test.dart b/test/user_profile_test.dart new file mode 100644 index 00000000..9b672ad5 --- /dev/null +++ b/test/user_profile_test.dart @@ -0,0 +1,125 @@ +/* + * Unit tests for the API class + */ + +import "package:test/test.dart"; +import "package:inventree/user_profile.dart"; + +void main() { + + setUp(() async { + // Ensure we have a user profile available + // This profile will match the dockerized InvenTree setup, running locally + + // To start with, there should not be *any* profiles available + var profiles = await UserProfileDBManager().getAllProfiles(); + + for (var prf in profiles) { + await UserProfileDBManager().deleteProfile(prf); + } + + // Check that there are *no* profiles in the database + profiles = await UserProfileDBManager().getAllProfiles(); + expect(profiles.length, equals(0)); + + // Now, create one! + bool result = await UserProfileDBManager().addProfile(UserProfile( + name: "Test Profile", + username: "testuser", + password: "testpassword""", + server: "http://localhost:12345", + selected: true, + )); + + expect(result, equals(true)); + + // Ensure we have one profile available + // expect(profiles.length, equals(1)); + profiles = await UserProfileDBManager().getAllProfiles(); + + expect(profiles.length, equals(1)); + + int key = -1; + + // Find the first available profile + for (var p in profiles) { + if (p.key != null) { + key = p.key ?? key; + break; + } + } + + // Select the profile + await UserProfileDBManager().selectProfile(key); + }); + + // Run a set of tests for user profile functionality + group("Profile Tests:", () { + + test("Add Invalid Profiles", () async { + // Add a profile with missing data + bool result = await UserProfileDBManager().addProfile( + UserProfile( + username: "what", + password: "why", + ) + ); + + expect(result, equals(false)); + + // Add a profile with a name that already exists + result = await UserProfileDBManager().addProfile( + UserProfile( + name: "Test Profile", + username: "xyz", + password: "hunter42", + ) + ); + + expect(result, equals(false)); + + // Check that the number of protocols available is still the same + var profiles = await UserProfileDBManager().getAllProfiles(); + + expect(profiles.length, equals(1)); + }); + + test("Profile Name Check", () async { + bool result = await UserProfileDBManager().profileNameExists("doesnotexist"); + expect(result, equals(false)); + + result = await UserProfileDBManager().profileNameExists("Test Profile"); + expect(result, equals(true)); + }); + + test("Select Profile", () async { + // Ensure that we can select a user profile + final prf = await UserProfileDBManager().getSelectedProfile(); + + expect(prf, isNot(null)); + + if (prf != null) { + UserProfile p = prf; + + expect(p.name, equals("Test Profile")); + expect(p.username, equals("testuser")); + expect(p.password, equals("testpassword")); + expect(p.server, equals("http://localhost:12345")); + + expect(p.toString(), equals("<${p.key}> Test Profile : http://localhost:12345 - testuser:testpassword")); + + // Test that we can update the profile + p.name = "different name"; + + bool result = await UserProfileDBManager().updateProfile(p); + expect(result, equals(true)); + + // Trying to update with an invalid value will fail! + p.password = ""; + result = await UserProfileDBManager().updateProfile(p); + expect(result, equals(false)); + } + }); + }); + +} \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart index 3c73102a..4e838ac5 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,9 +5,9 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import "package:flutter_test/flutter_test.dart"; +// import "package:flutter_test/flutter_test.dart"; void main() { - testWidgets("Counter increments smoke test", (WidgetTester tester) async { - }); + // testWidgets("Counter increments smoke test", (WidgetTester tester) async { + // }); } From 9f7e0a8dbfe4d71493bfebd9f0d70f739ef00817 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 10:12:31 +1000 Subject: [PATCH 3/7] Merge remote-tracking branch 'origin/l10n_master' (cherry picked from commit 55f713e3aa31c254002f00feb62dca4170bca176) --- lib/l10n/hu_HU/app_hu_HU.arb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/l10n/hu_HU/app_hu_HU.arb b/lib/l10n/hu_HU/app_hu_HU.arb index 4075b803..da164801 100644 --- a/lib/l10n/hu_HU/app_hu_HU.arb +++ b/lib/l10n/hu_HU/app_hu_HU.arb @@ -545,6 +545,8 @@ "@search": { "description": "search" }, + "searching": "Keresés", + "@searching": {}, "searchLocation": "Hely keresése", "@searchLocation": {}, "searchParts": "Alkatrészek keresése", From bf722d6b76bf0356dd52ebe6155cdcffefc0a0e8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 22 May 2022 16:26:57 +1000 Subject: [PATCH 4/7] Merge pull request #140 from inventree/legacy-api Remove support for legacy stock transfer API code (cherry picked from commit 333e5bb41d24466077d3f1c2de15cc03baab276e) --- assets/release_notes.md | 3 + lib/api.dart | 5 +- lib/api_form.dart | 70 +++++- lib/inventree/stock.dart | 37 +-- lib/widget/stock_detail.dart | 438 +++++++++-------------------------- test/api_test.dart | 1 - 6 files changed, 189 insertions(+), 365 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index e240c26d..23c44dcc 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -5,6 +5,9 @@ --- - Fixes issue which prevented text input in search window +- Remove support for legacy stock adjustment API +- App now requires server API version 20 (or newer) +- Updated translation files ### 0.7.0 - May 2022 --- diff --git a/lib/api.dart b/lib/api.dart index 52bd29a1..99b42424 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -144,7 +144,7 @@ class InvenTreeAPI { InvenTreeAPI._internal(); // Minimum required API version for server - static const _minApiVersion = 7; + static const _minApiVersion = 20; bool _strictHttps = false; @@ -294,9 +294,6 @@ class InvenTreeAPI { // API endpoint for receiving purchase order line items was introduced in v12 bool get supportsPoReceive => apiVersion >= 12; - // "Modern" API transactions were implemented in API v14 - bool get supportsModernStockTransactions => apiVersion >= 14; - /* * Connect to the remote InvenTree server: * diff --git a/lib/api_form.dart b/lib/api_form.dart index 588658bc..04a51dde 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -1161,6 +1161,72 @@ class _APIFormWidgetState extends State { nonFieldErrors = errors; } + /* Check for errors relating to an *unhandled* field name + * These errors will not be displayed and potentially confuse the user + * So, we need to know if these are ever happening + */ + void checkInvalidErrors(APIResponse response) { + var errors = response.asMap(); + + for (String fieldName in errors.keys) { + + bool match = false; + + switch (fieldName) { + case "__all__": + case "non_field_errors": + case "errors": + // ignore these global fields + match = true; + continue; + default: + for (var field in fields) { + + // Hidden fields can't display errors, so we won't match + if (field.hidden) { + continue; + } + + if (field.name == fieldName) { + // Direct Match found! + match = true; + break; + } else if (field.parent == fieldName) { + + var error = errors[fieldName]; + + if (error is List) { + for (var el in error) { + if (el is Map && el.containsKey(field.name)) { + match = true; + break; + } + } + } else if (error is Map && error.containsKey(field.name)) { + match = true; + break; + } + } + } + + break; + } + + if (!match) { + // Match for an unknown / unsupported field + sentryReportMessage( + "API form returned error for unsupported field", + context: { + "url": response.url, + "status_code": response.statusCode.toString(), + "field": fieldName, + "error_message": response.data.toString(), + } + ); + } + } + } + /* * Submit the form data to the server, and handle the results */ @@ -1234,8 +1300,6 @@ class _APIFormWidgetState extends State { // Hide this form Navigator.pop(context); - // TODO: Display a snackBar - if (successFunc != null) { // Ensure the response is a valid JSON structure @@ -1263,7 +1327,7 @@ class _APIFormWidgetState extends State { } extractNonFieldErrors(response); - + checkInvalidErrors(response); break; case 401: showSnackIcon( diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 2bb741f2..50b5ebe7 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -517,7 +517,6 @@ class InvenTreeStockItem extends InvenTreeModel { * - Remove * - Count */ - // TODO: Remove this function when we deprecate support for the old API Future adjustStock(BuildContext context, String endpoint, double q, {String? notes, int? location}) async { // Serialized stock cannot be adjusted (unless it is a "transfer") @@ -532,46 +531,29 @@ class InvenTreeStockItem extends InvenTreeModel { Map data = {}; - // Note: Format of adjustment API was updated in API v14 - if (api.supportsModernStockTransactions) { - // Modern (> 14) API - data = { - "items": [ - { - "pk": "${pk}", - "quantity": "${quantity}", - } - ], - }; - } else { - // Legacy (<= 14) API - data = { - "item": { + data = { + "items": [ + { "pk": "${pk}", "quantity": "${quantity}", - }, - }; - } - - data["notes"] = notes ?? ""; + } + ], + "notes": notes ?? "", + }; if (location != null) { data["location"] = location; } - // Expected API return code depends on server API version - final int expected_response = api.supportsModernStockTransactions ? 201 : 200; - var response = await api.post( endpoint, body: data, - expectedStatusCode: expected_response, + expectedStatusCode: 200, ); return response.isValid(); } - // TODO: Remove this function when we deprecate support for the old API Future countStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/count/", q, notes: notes); @@ -579,7 +561,6 @@ class InvenTreeStockItem extends InvenTreeModel { return result; } - // TODO: Remove this function when we deprecate support for the old API Future addStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/add/", q, notes: notes); @@ -587,7 +568,6 @@ class InvenTreeStockItem extends InvenTreeModel { return result; } - // TODO: Remove this function when we deprecate support for the old API Future removeStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes); @@ -595,7 +575,6 @@ class InvenTreeStockItem extends InvenTreeModel { return result; } - // TODO: Remove this function when we deprecate support for the old API Future transferStock(BuildContext context, int location, {double? quantity, String? notes}) async { double q = this.quantity; diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index e4773755..62058f2f 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -1,15 +1,12 @@ import "package:flutter/material.dart"; -import "package:dropdown_search/dropdown_search.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/barcode.dart"; -import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/part.dart"; import "package:inventree/widget/dialogs.dart"; -import "package:inventree/widget/fields.dart"; import "package:inventree/widget/location_display.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/progress.dart"; @@ -42,14 +39,6 @@ class _StockItemDisplayState extends RefreshableState { @override String getAppBarTitle(BuildContext context) => L10().stockItem; - final TextEditingController _quantityController = TextEditingController(); - final TextEditingController _notesController = TextEditingController(); - - final _addStockKey = GlobalKey(); - final _removeStockKey = GlobalKey(); - final _countStockKey = GlobalKey(); - final _moveStockKey = GlobalKey(); - bool stockShowHistory = false; @override @@ -295,76 +284,37 @@ class _StockItemDisplayState extends RefreshableState { } - Future _addStock() async { - - double quantity = double.parse(_quantityController.text); - _quantityController.clear(); - - final bool result = await item.addStock(context, quantity, notes: _notesController.text); - _notesController.clear(); - - _stockUpdateMessage(result); - - refresh(context); - } - + /* + * Launch a dialog to 'add' quantity to this StockItem + */ Future _addStockDialog() async { - // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportsModernStockTransactions) { - - Map fields = { - "pk": { - "parent": "items", - "nested": true, - "hidden": true, - "value": item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": 0, - }, - "notes": {}, - }; - - launchApiForm( - context, - L10().addStock, - InvenTreeStockItem.addStockUrl(), - fields, - method: "POST", - icon: FontAwesomeIcons.plusCircle, - onSuccess: (data) async { - _stockUpdateMessage(true); - refresh(context); - } - ); - - return; - } - - _quantityController.clear(); - _notesController.clear(); - - showFormDialog( L10().addStock, - key: _addStockKey, - callback: () { - _addStock(); + Map fields = { + "pk": { + "parent": "items", + "nested": true, + "hidden": true, + "value": item.pk, }, - fields: [ - Text("Current stock: ${item.quantity}"), - QuantityField( - label: L10().addStock, - controller: _quantityController, - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().notes, - ), - controller: _notesController, - ) - ], + "quantity": { + "parent": "items", + "nested": true, + "value": 0, + }, + "notes": {}, + }; + + launchApiForm( + context, + L10().addStock, + InvenTreeStockItem.addStockUrl(), + fields, + method: "POST", + icon: FontAwesomeIcons.plusCircle, + onSuccess: (data) async { + _stockUpdateMessage(true); + refresh(context); + } ); } @@ -375,149 +325,68 @@ class _StockItemDisplayState extends RefreshableState { } } - Future _removeStock() async { - - double quantity = double.parse(_quantityController.text); - _quantityController.clear(); - - final bool result = await item.removeStock(context, quantity, notes: _notesController.text); - - _stockUpdateMessage(result); - - refresh(context); - - } - + /* + * Launch a dialog to 'remove' quantity from this StockItem + */ void _removeStockDialog() { - // TODO: In future, deprecate support for the older API - if (InvenTreeAPI().supportsModernStockTransactions) { - Map fields = { - "pk": { - "parent": "items", - "nested": true, - "hidden": true, - "value": item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": 0, - }, - "notes": {}, - }; + Map fields = { + "pk": { + "parent": "items", + "nested": true, + "hidden": true, + "value": item.pk, + }, + "quantity": { + "parent": "items", + "nested": true, + "value": 0, + }, + "notes": {}, + }; - launchApiForm( - context, - L10().removeStock, - InvenTreeStockItem.removeStockUrl(), - fields, - method: "POST", - icon: FontAwesomeIcons.minusCircle, - onSuccess: (data) async { - _stockUpdateMessage(true); - refresh(context); - } - ); - - return; - } - - _quantityController.clear(); - _notesController.clear(); - - showFormDialog(L10().removeStock, - key: _removeStockKey, - callback: () { - _removeStock(); - }, - fields: [ - Text("Current stock: ${item.quantity}"), - QuantityField( - label: L10().removeStock, - controller: _quantityController, - max: item.quantity, - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().notes, - ), - controller: _notesController, - ), - ], + launchApiForm( + context, + L10().removeStock, + InvenTreeStockItem.removeStockUrl(), + fields, + method: "POST", + icon: FontAwesomeIcons.minusCircle, + onSuccess: (data) async { + _stockUpdateMessage(true); + refresh(context); + } ); } - Future _countStock() async { - - double quantity = double.parse(_quantityController.text); - _quantityController.clear(); - - final bool result = await item.countStock(context, quantity, notes: _notesController.text); - - _stockUpdateMessage(result); - - refresh(context); - } - Future _countStockDialog() async { - // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportsModernStockTransactions) { - - Map fields = { - "pk": { - "parent": "items", - "nested": true, - "hidden": true, - "value": item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": item.quantity, - }, - "notes": {}, - }; - - launchApiForm( - context, - L10().countStock, - InvenTreeStockItem.countStockUrl(), - fields, - method: "POST", - icon: FontAwesomeIcons.clipboardCheck, - onSuccess: (data) async { - _stockUpdateMessage(true); - refresh(context); - } - ); - - return; - } - - _quantityController.text = item.quantity.toString(); - _notesController.clear(); - - showFormDialog(L10().countStock, - key: _countStockKey, - callback: () { - _countStock(); + Map fields = { + "pk": { + "parent": "items", + "nested": true, + "hidden": true, + "value": item.pk, }, - acceptText: L10().count, - fields: [ - QuantityField( - label: L10().countStock, - hint: "${item.quantityString}", - controller: _quantityController, - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().notes, - ), - controller: _notesController, - ) - ] + "quantity": { + "parent": "items", + "nested": true, + "value": item.quantity, + }, + "notes": {}, + }; + + launchApiForm( + context, + L10().countStock, + InvenTreeStockItem.countStockUrl(), + fields, + method: "POST", + icon: FontAwesomeIcons.clipboardCheck, + onSuccess: (data) async { + _stockUpdateMessage(true); + refresh(context); + } ); } @@ -542,130 +411,43 @@ class _StockItemDisplayState extends RefreshableState { } - // TODO: Delete this function once support for old API is deprecated - Future _transferStock(int locationId) async { - - double quantity = double.tryParse(_quantityController.text) ?? item.quantity; - String notes = _notesController.text; - - _quantityController.clear(); - _notesController.clear(); - - var result = await item.transferStock(context, locationId, quantity: quantity, notes: notes); - - refresh(context); - - if (result) { - showSnackIcon(L10().stockItemTransferred, success: true); - } - } - /* * Launches an API Form to transfer this stock item to a new location */ Future _transferStockDialog(BuildContext context) async { - // TODO: In future, deprecate support for older API - if (InvenTreeAPI().supportsModernStockTransactions) { + Map fields = { + "pk": { + "parent": "items", + "nested": true, + "hidden": true, + "value": item.pk, + }, + "quantity": { + "parent": "items", + "nested": true, + "value": item.quantity, + }, + "location": {}, + "notes": {}, + }; - Map fields = { - "pk": { - "parent": "items", - "nested": true, - "hidden": true, - "value": item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": item.quantity, - }, - "location": {}, - "notes": {}, - }; - - launchApiForm( - context, - L10().transferStock, - InvenTreeStockItem.transferStockUrl(), - fields, - method: "POST", - icon: FontAwesomeIcons.dolly, - onSuccess: (data) async { - _stockUpdateMessage(true); - refresh(context); - } - ); - - return; + if (item.isSerialized()) { + // Prevent editing of 'quantity' field if the item is serialized + fields["quantity"]["hidden"] = true; } - int? location_pk; - - _quantityController.text = "${item.quantity}"; - - showFormDialog(L10().transferStock, - key: _moveStockKey, - callback: () { - var _pk = location_pk; - - if (_pk != null) { - _transferStock(_pk); - } - }, - fields: [ - QuantityField( - label: L10().quantity, - controller: _quantityController, - max: item.quantity, - ), - DropdownSearch( - mode: Mode.BOTTOM_SHEET, - showSelectedItem: false, - autoFocusSearchBox: true, - selectedItem: null, - errorBuilder: (context, entry, exception) { - print("entry: $entry"); - print(exception.toString()); - - return Text( - exception.toString(), - style: TextStyle( - fontSize: 10, - ) - ); - }, - onFind: (String filter) async { - - final results = await InvenTreeStockLocation().search(filter); - - List items = []; - - for (InvenTreeModel loc in results) { - if (loc is InvenTreeStockLocation) { - items.add(loc.jsondata); - } - } - - return items; - }, - label: L10().stockLocation, - hint: L10().searchLocation, - onChanged: null, - itemAsString: (dynamic location) { - return (location["pathstring"] ?? "") as String; - }, - onSaved: (dynamic location) { - if (location == null) { - location_pk = null; - } else { - location_pk = location["pk"] as int; - } - }, - isFilteredOnline: true, - showSearchBox: true, - ), - ], + launchApiForm( + context, + L10().transferStock, + InvenTreeStockItem.transferStockUrl(), + fields, + method: "POST", + icon: FontAwesomeIcons.dolly, + onSuccess: (data) async { + _stockUpdateMessage(true); + refresh(context); + } ); } diff --git a/test/api_test.dart b/test/api_test.dart index b46cab64..4f9723bd 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -127,7 +127,6 @@ void main() { assert(api.apiVersion >= 50); assert(api.supportsSettings); assert(api.supportsNotifications); - assert(api.supportsModernStockTransactions); assert(api.supportsPoReceive); // Check available permissions From bbe56aba5526845e8c8948153bf6ddf198fba921 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 16:44:11 +1000 Subject: [PATCH 5/7] Bump version number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 33932d52..4a04b5d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: inventree description: InvenTree stock management -version: 0.7.0+41 +version: 0.7.1+42 environment: sdk: ">=2.16.0 <3.0.0" From 7d24e1818fd9e4b9512aaff5d946a7d0aa2bc365 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 22 May 2022 10:40:01 +1000 Subject: [PATCH 6/7] Merge pull request #138 from inventree/stacktrace-data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attempt to capture stacktrace data when uploading a custom message to… (cherry picked from commit 6f885d3a5ccca56cc1fca574031505b920d1b5cf) --- lib/inventree/sentry.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/inventree/sentry.dart b/lib/inventree/sentry.dart index cd05a14f..0e8e6966 100644 --- a/lib/inventree/sentry.dart +++ b/lib/inventree/sentry.dart @@ -129,6 +129,9 @@ Future sentryReportMessage(String message, {Map? context}) if (context != null) { scope.setExtra("context", context); } + + // Catch stacktrace data if possible + scope.setExtra("stacktrace", StackTrace.current.toString()); }); try { From 6e93b9c7faa3685c76458ccb9509416d0c026ab2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 May 2022 16:57:52 +1000 Subject: [PATCH 7/7] Increment build number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4a04b5d1..96f91d04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: inventree description: InvenTree stock management -version: 0.7.1+42 +version: 0.7.1+43 environment: sdk: ">=2.16.0 <3.0.0"