diff --git a/README.md b/README.md index e65ae43..6ad101a 100644 --- a/README.md +++ b/README.md @@ -92,4 +92,12 @@ If you want to get an easier command, you can create an alias into Zsh or Bash p ``` alias kassakuitti='dart run $HOME/Documents/dart_kassakuitti_cli/bin/dart_kassakuitti_cli.dart' +``` + +## Generate `hive_product.g.dart` file + +You can generate `hive_product.g.dart` file by running: + +``` +dart run build_runner build ``` \ No newline at end of file diff --git a/bin/dart_kassakuitti_cli.dart b/bin/dart_kassakuitti_cli.dart index bbc2dc5..b4a7a52 100644 --- a/bin/dart_kassakuitti_cli.dart +++ b/bin/dart_kassakuitti_cli.dart @@ -38,7 +38,7 @@ void main(List arguments) async { var eanProducts = await readEANProducts( selectedHtmlFile, ShopSelector.sKaupat, csvFilesPath); - eanHandler(receiptProducts, eanProducts.toList()); + await eanHandler(receiptProducts, eanProducts.toList()); receiptProducts2CSV(receiptProducts, csvFilesPath); eanProducts2CSV(eanProducts, csvFilesPath, ShopSelector.sKaupat.name); diff --git a/bin/models/hive_product.dart b/bin/models/hive_product.dart new file mode 100644 index 0000000..d7a9265 --- /dev/null +++ b/bin/models/hive_product.dart @@ -0,0 +1,19 @@ +import 'package:hive/hive.dart'; + +part 'hive_product.g.dart'; + +@HiveType(typeId: 0) +class HiveProduct extends HiveObject { + @HiveField(0) + String receiptName; + + @HiveField(1) + String eanName; + + HiveProduct({required this.receiptName, required this.eanName}); + + @override + String toString() { + return 'ReceiptName: $receiptName, eanName: $eanName'; + } +} diff --git a/bin/models/hive_product.g.dart b/bin/models/hive_product.g.dart new file mode 100644 index 0000000..432f52c --- /dev/null +++ b/bin/models/hive_product.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'hive_product.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class HiveProductAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + HiveProduct read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return HiveProduct( + receiptName: fields[0] as String, + eanName: fields[1] as String, + ); + } + + @override + void write(BinaryWriter writer, HiveProduct obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.receiptName) + ..writeByte(1) + ..write(obj.eanName); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HiveProductAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/bin/specific/s_kaupat/ean_handler.dart b/bin/specific/s_kaupat/ean_handler.dart index 001f4e6..232362d 100644 --- a/bin/specific/s_kaupat/ean_handler.dart +++ b/bin/specific/s_kaupat/ean_handler.dart @@ -1,22 +1,42 @@ import 'dart:io'; +import 'package:hive/hive.dart'; + import '../../models/ean_product.dart'; +import '../../models/hive_product.dart'; import '../../models/receipt_product.dart'; +import '../../utils/ansipen_helper.dart'; + +const _kHiveBoxName = 'hiveProducts'; + +/// Handles the EAN products. +Future eanHandler( + List receiptProducts, List eanProducts) async { + List nonFoundReceiptProducts = []; + List nonFoundReceiptProducts2 = []; + + Box hiveProducts = await _initializeHiveProducts(); -void eanHandler( - List receiptProducts, List eanProducts) { print('\nThe first round begins!'); - print('Statistics: ${receiptProducts.length} receiptProducts, ' + print('Statistics:'); + print(peachPen().write('Amount of hive products: ${hiveProducts.length}')); + print('${receiptProducts.length} receiptProducts, ' '${eanProducts.length} eanProducts\n'); - List nonFoundReceiptProducts = []; for (var receiptProduct in receiptProducts) { print(receiptProduct); + String? eanProductName = + _filterHiveProducts(hiveProducts.values, receiptProduct.name); + print(peachPen().write('EAN product name: $eanProductName')); + var filteredEanProducts = - _filterEANProducts(receiptProduct.name, eanProducts); + _filterEANProducts(receiptProduct.name, eanProducts, eanProductName); + + _handleFoundCases( + receiptProduct, filteredEanProducts, eanProducts, hiveProducts); - _handleFoundCases(receiptProduct, filteredEanProducts, eanProducts); + print(peachPen().write('Amount of hive products: ${hiveProducts.length}')); if (filteredEanProducts.isEmpty) { print('\tNo product found for the 1st round.'); @@ -39,55 +59,112 @@ void eanHandler( continue; } - var filteredEanProducts = - _filterEANProducts(splittedReceiptProcuctNames[0], eanProducts); + String? eanProductName = + _filterHiveProducts(hiveProducts.values, nonFoundReceiptProduct.name); + print(peachPen().write('EAN product name: $eanProductName')); - if (filteredEanProducts.length > 2 && - splittedReceiptProcuctNames.length > 1) { - filteredEanProducts = - _filterEANProducts(splittedReceiptProcuctNames[1], eanProducts); - } + var filteredEanProducts = _filterEANProducts( + splittedReceiptProcuctNames[0], eanProducts, eanProductName); - _handleFoundCases(nonFoundReceiptProduct, filteredEanProducts, eanProducts); + _handleFoundCases( + nonFoundReceiptProduct, filteredEanProducts, eanProducts, hiveProducts); if (filteredEanProducts.isEmpty) { print('\tNo product found for the 2nd round.'); + nonFoundReceiptProducts2.add(nonFoundReceiptProduct); } } print('\nFinished!'); - print('Only ${eanProducts.length} unknown eanProducts left.'); + print('${nonFoundReceiptProducts2.length < 10 ? 'Only ' : ''}' + '${nonFoundReceiptProducts2.length} unknown receipt ' + '${nonFoundReceiptProducts2.length == 1 ? 'product' : 'products'} ' + 'left.'); + print(peachPen().write('Amount of hive products: ${hiveProducts.length}')); + + hiveProducts.close(); } -List _filterEANProducts( - String receiptProductName, List eanProducts) { +String? _filterHiveProducts( + Iterable hiveProductsValues, String receiptProductName) { + String? eanProductName; + + var filteredHiveProducts = hiveProductsValues + .where((hiveProduct) => hiveProduct.receiptName == receiptProductName); + /* + If the receipt product is already in the hive products, + get the ean product name from the hive product. + */ + if (filteredHiveProducts.isNotEmpty) { + print(greenPen().write( + '\tFound ${filteredHiveProducts.length} pcs in the hive products!')); + eanProductName = filteredHiveProducts.first.eanName; + print('\teanProductName in Hive: $eanProductName'); + } + return eanProductName; +} + +Future> _initializeHiveProducts() async { + Hive.init(Directory.current.path); + Hive.registerAdapter(HiveProductAdapter()); + return await Hive.openBox(_kHiveBoxName); +} + +List _filterEANProducts(String receiptProductName, + List eanProducts, String? eanProductName) { + /* + If eanProductName is not null, + filter the ean products by the ean product name. + */ + if (eanProductName != null) { + return eanProducts + .where((eanProduct) => + eanProduct.name.toLowerCase() == eanProductName.toLowerCase()) + .toList(); + } return eanProducts - .where((eanProduct) => - eanProduct.name.toLowerCase().contains(receiptProductName)) + .where((eanProduct) => eanProduct.name + .toLowerCase() + .contains(receiptProductName.toLowerCase())) .toList(); } -void _handleFoundCases(ReceiptProduct receiptProduct, - List filteredEanProducts, List origEanProducts) { +void _handleFoundCases( + ReceiptProduct receiptProduct, + List filteredEanProducts, + List origEanProducts, + Box hiveProducts) { if (filteredEanProducts.length == 1) { - print('\tFound one product:'); - print('\t\t${filteredEanProducts[0]}'); + print(greenPen().write('\tFound one product:')); + print('\t\t${filteredEanProducts.first}'); - receiptProduct.eanCode = filteredEanProducts[0].ean; - origEanProducts.remove(filteredEanProducts[0]); + receiptProduct.eanCode = filteredEanProducts.first.ean; } else if (filteredEanProducts.length > 1) { - print( - '\tFound multiple products (${filteredEanProducts.length} possible choices):'); - - for (var filteredReceiptProduct in filteredEanProducts) { - print('\t\t$filteredReceiptProduct'); - print('\t\tIs this the right product? (y/n)'); - var answer = stdin.readLineSync(); - if (answer?.toLowerCase() == 'y') { - receiptProduct.eanCode = filteredReceiptProduct.ean; - origEanProducts.remove(filteredReceiptProduct); - break; - } + print(redPen() + .write('\tFound multiple products (${filteredEanProducts.length}' + ' possible choices):')); + + for (var i = 0; i < filteredEanProducts.length; i++) { + print('\t\t${i + 1}. ${filteredEanProducts[i]}'); + } + print('\nPlease enter the number of the product you want to select: '); + var answer = stdin.readLineSync(); + + if (answer!.isNotEmpty && + int.parse(answer) <= filteredEanProducts.length && + int.parse(answer) > 0) { + var selectedEanProduct = filteredEanProducts[int.parse(answer) - 1]; + + print(greenPen().write('\tYou selected: $selectedEanProduct')); + + receiptProduct.eanCode = selectedEanProduct.ean; + + hiveProducts.add(HiveProduct( + receiptName: receiptProduct.name, eanName: selectedEanProduct.name)); + print( + peachPen().write('Amount of hive products: ${hiveProducts.length}')); + } else { + print('\tNo product selected.'); } } } diff --git a/bin/specific/s_kaupat/strings_to_receipt_products.dart b/bin/specific/s_kaupat/strings_to_receipt_products.dart index 249f220..f450f5a 100644 --- a/bin/specific/s_kaupat/strings_to_receipt_products.dart +++ b/bin/specific/s_kaupat/strings_to_receipt_products.dart @@ -2,12 +2,12 @@ import '../../utils/extensions/double_extension.dart'; import '../../utils/line_helper.dart'; import '../../models/receipt_product.dart'; -/// Goes through the list of lines and returns a list of products. -List strings2ReceiptProducts(List lines) { +/// Goes through the list of rows and returns a list of products. +List strings2ReceiptProducts(List rows) { var helper = LineHelper(); List products = []; - for (var item in lines) { + for (var item in rows) { item = item.trim(); item = item.toLowerCase(); @@ -15,11 +15,11 @@ List strings2ReceiptProducts(List lines) { if (item.contains('----------')) { break; } - // Refund line: + // Refund row: else if (item.contains('palautus')) { helper.previousLine = PreviousLine.refund; } - // When previous line was a refund line, skip next two lines: + // When the previous row was a refund row, skip next two rows: else if (helper.previousLine == PreviousLine.refund) { if (helper.calcLines != 1) { helper.calcLines++; @@ -28,7 +28,7 @@ List strings2ReceiptProducts(List lines) { helper.previousLine = PreviousLine.notSet; } } - // A discount line: + // A discount row: else if (item.contains('alennus')) { var items = item.split(RegExp(r'\s{12,33}')); var discountPrice = @@ -42,7 +42,7 @@ List strings2ReceiptProducts(List lines) { var discountedPrice = (origTotalPriceAsDouble - discountPriceAsDouble).toPrecision(2); var discountedPriceAsString = - discountedPrice.toStringAsFixed(2).replaceAll(RegExp(r'\.'), ','); + discountedPrice.toString().replaceAll(RegExp(r'\.'), ','); if (lastProduct.quantity > 1) { var discountedPricePerUnit = (discountedPrice / lastProduct.quantity) @@ -55,7 +55,37 @@ List strings2ReceiptProducts(List lines) { lastProduct.totalPrice = discountedPriceAsString; lastProduct.discountCounted = 'yes'; } - // If a line starts with a digit, it is a quantity and price per unit row: + /* + A campaign row + (i.e. usually means that there's a mistake in the previous line): + */ + else if (item.contains('kampanja')) { + var items = item.split(RegExp(r'\s{12,33}')); + var campaignPrice = + items[1].replaceAll(RegExp(r'\-'), '').replaceAll(RegExp(r','), '.'); + var campaignPriceAsDouble = double.parse(campaignPrice); + + var lastProduct = products.last; + var origTotalPrice = lastProduct.totalPrice.replaceAll(RegExp(r','), '.'); + var origTotalPriceAsDouble = double.parse(origTotalPrice); + + var fixedPrice = + (origTotalPriceAsDouble - campaignPriceAsDouble).toPrecision(2); + var fixedPriceAsString = + fixedPrice.toString().replaceAll(RegExp(r'\.'), ','); + + if (lastProduct.quantity > 1) { + var fixedPricePerUnit = (fixedPrice / lastProduct.quantity) + .toPrecision(2) + .toString() + .replaceAll(RegExp(r'\.'), ','); + + lastProduct.pricePerUnit = fixedPricePerUnit; + } + + lastProduct.totalPrice = fixedPriceAsString; + } + // If a row starts with a digit, it is a quantity and price per unit row: else if (item.contains(RegExp(r'^\d'))) { var items = item.split(RegExp(r'\s{6,7}')); var quantity = @@ -68,7 +98,7 @@ List strings2ReceiptProducts(List lines) { lastProduct.pricePerUnit = pricePerUnit; } - // A "normal" line: + // A "normal" row: else { var items = item.split(RegExp(r'\s{11,33}')); diff --git a/bin/utils/ansipen_helper.dart b/bin/utils/ansipen_helper.dart new file mode 100644 index 0000000..e5b99b6 --- /dev/null +++ b/bin/utils/ansipen_helper.dart @@ -0,0 +1,19 @@ +import 'package:ansicolor/ansicolor.dart'; + +AnsiPen greenPen() { + return AnsiPen() + ..black(bold: true) + ..green(bold: true); +} + +AnsiPen redPen() { + return AnsiPen() + ..black(bold: true) + ..red(bold: true); +} + +AnsiPen peachPen() { + return AnsiPen() + ..black(bold: true) + ..rgb(r: 1.0, g: 0.8, b: 0.2); +} diff --git a/pubspec.lock b/pubspec.lock index 020c116..a61a8d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,27 @@ # 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: "38.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "3.4.1" + ansicolor: + dependency: "direct main" + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" args: dependency: "direct main" description: @@ -8,6 +29,69 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.10" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.2.0" charcode: dependency: transitive description: @@ -15,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" clock: dependency: transitive description: @@ -22,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" collection: dependency: transitive description: @@ -29,6 +127,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" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" csslib: dependency: transitive description: @@ -36,6 +148,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + hive: + dependency: "direct main" + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" html: dependency: "direct main" description: @@ -43,6 +211,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.15.0" + 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: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" intl: dependency: "direct main" description: @@ -50,6 +232,27 @@ 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: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" lints: dependency: "direct dev" description: @@ -57,6 +260,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + meta: + dependency: transitive + description: + name: meta + 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.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" path: dependency: "direct main" description: @@ -64,6 +302,55 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" source_span: dependency: transitive description: @@ -71,6 +358,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: @@ -85,6 +393,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.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.1.0" yaml: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2d3a442..bcc2cf0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,9 +11,13 @@ environment: # path: ^1.8.0 dev_dependencies: + build_runner: ^2.1.10 + hive_generator: ^1.1.2 lints: ^1.0.1 dependencies: + ansicolor: ^2.0.1 args: ^2.3.0 + hive: ^2.1.0 html: ^0.15.0 intl: ^0.17.0 path: ^1.8.1