Skip to content

Commit

Permalink
v1.0.25 - Foreign currency retranslation (#25)
Browse files Browse the repository at this point in the history
* v1.0.25 - Fix migrations

* v1.0.25 - Get accounts by attributes

* v1.0.25 - Link Capital Gains in Settings

* v1.0.25 - Show Entries by Account

* v1.0.25 - Foreign currency retranslation

* v1.0.25 - Auto adjust FCT alert

* v1.0.25 - InkWell looks better
  • Loading branch information
Donnie committed Jan 30, 2024
1 parent 04d7712 commit d219082
Show file tree
Hide file tree
Showing 31 changed files with 613 additions and 206 deletions.
33 changes: 4 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<a href="https://flutter.dev/" style="text-decoration:none" area-label="flutter">
<img src="https://img.shields.io/badge/Platform-Flutter%203.16.5-blue">
</a>
<a href="https://github.com/Donnie/Finease/releases/tag/v1.0.24" style="text-decoration:none" area-label="flutter">
<img src="https://img.shields.io/badge/Version-1.0.24-orange">
<a href="https://github.com/Donnie/Finease/releases/tag/v1.0.25" style="text-decoration:none" area-label="flutter">
<img src="https://img.shields.io/badge/Version-1.0.25-orange">
</a>
<a href="https://github.com/Donnie/Finease/actions/workflows/android_release.yml" style="text-decoration:none" area-label="flutter">
<img src="https://github.com/Donnie/Finease/actions/workflows/android_release.yml/badge.svg">
Expand All @@ -26,37 +26,12 @@

- Double Entry Accounting
- Easy export to Google Drive or WhatsApp/Telegram
- [Encryption with AES/CBC/PKCS7](https://github.com/Donnie/Finease/wiki/Encryption)
- [Foreign currency transactions with automated retranslation.](https://github.com/Donnie/Finease/wiki/Foreign-Currency-Retranslation-%E2%80%90-Gains-and-losses-in-foreign-currency)
- Totally offline*! 100% privacy commitment.

Cultivate discipline, enjoy ease of use, and control your financial data.

*\* needs internet only if you have multi currency accounts, to look up exchange rates from ECB.*

#### Encryption Feature

Finease now includes an advanced encryption feature to enhance data security. This feature ensures that all your financial data within the app is now encrypted during exporting it to Google Drive or other places, providing an additional layer of protection against unauthorized access.

**Technical Details**:

- Encryption Algorithm: The feature leverages the AES (Advanced Encryption Standard) algorithm, renowned for its balance between strong security and efficient performance. AES operates in CBC mode (Cipher Block Chaining), which ensures that each block of data is encrypted differently.
- Key Management: Encryption keys are generated using a secure, random process and are managed using a combination of hashing and salting techniques. This approach ensures that keys are unique and cannot be easily predicted or replicated.
- Data Integrity: Alongside encryption, the application implements HMAC (Hash-Based Message Authentication Code) to verify the integrity and authenticity of the data. This ensures that any tampering with the encrypted data can be detected.

**How It Works:**

- You have to turn on `Enable Encryption` under `Database` section from the Settings.
- The encryption feature automatically encrypts your database file during exports and uses the same password to decrypt it during importing.

**Tips:**

- Remember, encryption is only as strong as your password. Use a strong, unique password for your account.
- If you want to decrypt the DB file on your computer read the [directions provided here](decrypt.dart).

**Warnings:**

- Due to the nature of latest available encryption methods, your data cannot be encrypted while being in use by the App. Therefore, if your phone is compromised, your data and password shall be available to third party.
- If you provide the unencrypted database file to anyone they would get to know your DB password.
- If you forget your password, it might be impossible to recover your encrypted data. Always keep a backup of your password in a secure place.
- Avoid sharing your password or storing it in an insecure location.

> Made with ♥ in Berlin
61 changes: 58 additions & 3 deletions lib/db/accounts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,38 @@ class AccountService {
return account;
}

Future<Account> createForexRetransAccIfNotExist({
name = "Forex Retranslation",
}) async {
final dbClient = await _databaseHelper.db;

// Check if the account already exists
final String currency = await SettingService().getSetting(Setting.prefCurrency);
final List<Map<String, dynamic>> existingAccounts = await dbClient.query(
Accounts,
where: 'currency = ? AND name = ?',
whereArgs: [currency, name],
);

if (existingAccounts.isNotEmpty) {
// Account already exists, return the existing account
return Account.fromJson(existingAccounts.first);
} else {
// Account does not exist, create a new one
Account newAccount = Account(
balance: 0,
currency: currency,
name: name,
hidden: true,
type: AccountType.asset,
liquid: false,
);

// Persist the new account and return it
return await createAccount(newAccount);
}
}

Future<Account> createForexAccountIfNotExist(
String currency, {
double balance = 0,
Expand Down Expand Up @@ -92,10 +124,33 @@ class AccountService {
return null;
}

Future<List<Account>> getAllAccounts(bool hidden) async {
Future<List<Account>> getAllAccounts({
bool hidden = true,
bool liquid = false,
String? currency,
AccountType? type,
}) async {
final dbClient = await _databaseHelper.db;

var whereClause = hidden ? '' : 'WHERE hidden = 0';
List<String> conditions = [];
if (liquid) {
conditions.add('liquid = 1');
}
if (type != null) {
conditions.add("type = '${type.name}'");
}
if (currency != null) {
conditions.add("currency = '$currency'");
}
if (!hidden) {
conditions.add('hidden = 0');
}

String whereClause = '';
if (conditions.isNotEmpty) {
whereClause = 'WHERE ';
whereClause += conditions.join(' AND ');
}

String rawQuery = '''
SELECT
Expand Down Expand Up @@ -149,7 +204,6 @@ class AccountService {
// updates rates table
await currencyBoxService.init();
await currencyBoxService.updateRatesTable();
currencyBoxService.close();
}

List<String> conditions = ["type NOT IN ('income', 'expense')"];
Expand All @@ -159,6 +213,7 @@ class AccountService {
if (type != null) {
conditions.add("type = '${type.name}'");
}
conditions.add('hidden = 0');
String whereClause = conditions.join(' AND ');
String sql = '''
WITH CurrencyGroups AS (
Expand Down
4 changes: 0 additions & 4 deletions lib/db/currency.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,6 @@ class CurrencyBoxService {
rethrow;
}
}

void close() {
_box.close();
}
}

// ignore: non_constant_identifier_names
Expand Down
59 changes: 49 additions & 10 deletions lib/db/entries.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:finease/db/accounts.dart';
import 'package:finease/db/currency.dart';
import 'package:finease/db/db.dart';
import 'package:finease/db/settings.dart';

class EntryService {
final DatabaseHelper _databaseHelper;
Expand Down Expand Up @@ -97,36 +98,44 @@ class EntryService {
Future<List<Entry>> getAllEntries({
DateTime? startDate,
DateTime? endDate,
int? accountId,
}) async {
final dbClient = await _databaseHelper.db;

String whereClause = '';
List<String> conditions = [];
List<dynamic> whereArguments = [];
// If account id is provided, add it to the where clause
if (accountId != null) {
conditions.add('debit_account_id = ? OR credit_account_id = ?');
whereArguments.add(accountId);
whereArguments.add(accountId);
}

// If startDate is provided, add it to the where clause
if (startDate != null) {
whereClause += 'date >= ?';
conditions.add('date >= ?');
whereArguments.add(startDate.toIso8601String());
}

// If endDate is provided, add it to the where clause
if (endDate != null) {
if (whereClause.isNotEmpty) {
whereClause += ' AND ';
}
whereClause += 'date <= ?';
conditions.add('date <= ?');
whereArguments.add(endDate.toIso8601String());
}

String whereClause = '';
if (conditions.isNotEmpty) {
whereClause += conditions.join(' AND ');
}

// Fetch all entries according to the provided start and end dates
final List<Map<String, dynamic>> entriesData = await dbClient.query(
'Entries',
where: whereClause.isEmpty ? null : whereClause,
whereArgs: whereArguments.isEmpty ? null : whereArguments,
);

final List<Account> allAccounts =
await AccountService().getAllAccounts(true);
final List<Account> allAccounts = await AccountService().getAllAccounts();

// Create a map for quick account lookup by ID
var accountsMap = {for (var account in allAccounts) account.id: account};
Expand Down Expand Up @@ -161,7 +170,7 @@ class EntryService {
);
}

Future adjustFirstBalance(
Future<void> adjustFirstBalance(
int toAccountId, int fromAccountId, double balance) async {
if (balance == 0) {
return;
Expand All @@ -178,7 +187,7 @@ class EntryService {
await dbClient.insert('Entries', entry.toJson());
}

Future adjustFirstForexBalance(
Future<void> adjustFirstForexBalance(
int toAccountId, int fromAccountId, double balance) async {
if (balance == 0) {
return;
Expand All @@ -193,6 +202,28 @@ class EntryService {

await createForexEntry(entry);
}

Future<void> addCurrencyRetranslation(
double amount,
) async {
Account forexReTrans =
await AccountService().createForexRetransAccIfNotExist();

String? capG = await SettingService().getSetting(Setting.capitalGains);
int? capGains = int.tryParse(capG);
if (capGains == null) {
throw AccountLinkingException("Capital Gains account not linked");
}

Entry entry = Entry(
debitAccountId: capGains,
creditAccountId: forexReTrans.id!,
amount: amount,
notes: "Foreign Currency Retranslation",
);

await createEntry(entry);
}
}

class Entry {
Expand Down Expand Up @@ -245,3 +276,11 @@ class Entry {
};
}
}

class AccountLinkingException implements Exception {
final String message;
AccountLinkingException(this.message);

@override
String toString() => message;
}
3 changes: 0 additions & 3 deletions lib/db/migrations/b_add_indices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,5 @@ Future<void> bAddIndices(Database db) async {
CREATE INDEX idx_entries_notes ON Entries(notes);
CREATE INDEX idx_settings_key ON Settings(key);
CREATE INDEX idx_migrations_id ON Migrations(id);
''');
}

1 change: 0 additions & 1 deletion lib/db/months.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class MonthService {
// updates rates table
await currencyBoxService.init();
await currencyBoxService.updateRatesTable();
currencyBoxService.close();
}

final dbClient = await _databaseHelper.db;
Expand Down
7 changes: 4 additions & 3 deletions lib/db/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import 'package:finease/db/db.dart';
import 'package:sqflite/sqflite.dart';

enum Setting {
introDone,
userName,
accountSetup,
capitalGains, // account.id int
dbPassword,
introDone,
onboarded,
pastAccount,
prefCurrency,
useEncryption,
dbPassword,
userName,
}

typedef Settings = Map<Setting, String>;
Expand Down
2 changes: 1 addition & 1 deletion lib/pages/add_account/account_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class AddAccountBodyState extends State<AddAccountBody> {
),
),
const SizedBox(height: 16),
GestureDetector(
InkWell(
onTap: () => showCurrencyPicker(
context: context,
currencyFilter: SupportedCurrency.keys.toList(),
Expand Down
1 change: 0 additions & 1 deletion lib/pages/add_entry/entry_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ class AddEntryBodyState extends State<AddEntryBody> {
_showError(e);
val = false;
} finally {
if (!val) _currencyBoxService.close();
setState(() {
_useECBrate = val;
});
Expand Down
2 changes: 1 addition & 1 deletion lib/pages/add_entry/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class AddEntryScreenState extends State<AddEntryScreen> {
}

Future<void> _fetchAccounts() async {
final accounts = await _accountService.getAllAccounts(false);
final accounts = await _accountService.getAllAccounts(hidden: false);
accounts.sort((a, b) => a.name.compareTo(b.name));

final curr = await _settingService.getSetting(Setting.prefCurrency);
Expand Down
2 changes: 2 additions & 0 deletions lib/pages/add_info/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class _AddInfoPageState extends State<AddInfoPage> {
if (_formState.currentState!.validate()) {
context.go(RoutesName.setupAccounts.path);
await _settingService.setSetting(Setting.userName, _name.text);

Account account = await _accountService.createAccount(Account(
balance: 0,
currency: _currency.text,
Expand Down Expand Up @@ -91,6 +92,7 @@ class _AddInfoPageState extends State<AddInfoPage> {
// next page needs the currency
await _settingService.setSetting(
Setting.prefCurrency, _currency.text);

saveForm();
},
extendedPadding: const EdgeInsets.symmetric(horizontal: 24),
Expand Down
2 changes: 1 addition & 1 deletion lib/pages/add_info/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class AddInfoBody extends StatelessWidget {
},
),
const SizedBox(height: 32),
GestureDetector(
InkWell(
onTap: () => showCurrencyPicker(
context: context,
currencyFilter: SupportedCurrency.keys.toList(),
Expand Down
2 changes: 1 addition & 1 deletion lib/pages/edit_account/account_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class EditAccountBody extends StatelessWidget {
Visibility(
visible: account.deletable,
child: Column(children: [
GestureDetector(
InkWell(
onTap: () => showCurrencyPicker(
context: context,
currencyFilter: SupportedCurrency.keys.toList(),
Expand Down
Loading

0 comments on commit d219082

Please sign in to comment.