1 | <?php |
||
2 | |||
3 | /** |
||
4 | * UpgradeDatabase.php |
||
5 | * Copyright (c) 2018 [email protected] |
||
6 | * |
||
7 | * This file is part of Firefly III. |
||
8 | * |
||
9 | * Firefly III is free software: you can redistribute it and/or modify |
||
10 | * it under the terms of the GNU General Public License as published by |
||
11 | * the Free Software Foundation, either version 3 of the License, or |
||
12 | * (at your option) any later version. |
||
13 | * |
||
14 | * Firefly III is distributed in the hope that it will be useful, |
||
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
17 | * GNU General Public License for more details. |
||
18 | * |
||
19 | * You should have received a copy of the GNU General Public License |
||
20 | * along with Firefly III. If not, see <http://www.gnu.org/licenses/>. |
||
21 | */ |
||
22 | |||
23 | /** @noinspection MultipleReturnStatementsInspection */ |
||
24 | /** @noinspection PhpStaticAsDynamicMethodCallInspection */ |
||
25 | /** @noinspection PhpDynamicAsStaticMethodCallInspection */ |
||
26 | |||
27 | declare(strict_types=1); |
||
28 | |||
29 | namespace FireflyIII\Console\Commands; |
||
30 | |||
31 | use DB; |
||
32 | use Exception; |
||
33 | use FireflyIII\Models\Account; |
||
34 | use FireflyIII\Models\AccountMeta; |
||
35 | use FireflyIII\Models\AccountType; |
||
36 | use FireflyIII\Models\Attachment; |
||
37 | use FireflyIII\Models\Bill; |
||
38 | use FireflyIII\Models\BudgetLimit; |
||
39 | use FireflyIII\Models\Note; |
||
40 | use FireflyIII\Models\Preference; |
||
41 | use FireflyIII\Models\Rule; |
||
42 | use FireflyIII\Models\RuleAction; |
||
43 | use FireflyIII\Models\RuleGroup; |
||
44 | use FireflyIII\Models\RuleTrigger; |
||
45 | use FireflyIII\Models\Transaction; |
||
46 | use FireflyIII\Models\TransactionCurrency; |
||
47 | use FireflyIII\Models\TransactionJournal; |
||
48 | use FireflyIII\Models\TransactionJournalMeta; |
||
49 | use FireflyIII\Models\TransactionType; |
||
50 | use FireflyIII\Repositories\Account\AccountRepositoryInterface; |
||
51 | use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; |
||
52 | use FireflyIII\Repositories\Journal\JournalRepositoryInterface; |
||
53 | use FireflyIII\User; |
||
54 | use Illuminate\Console\Command; |
||
55 | use Illuminate\Database\QueryException; |
||
56 | use Illuminate\Support\Collection; |
||
57 | use Log; |
||
58 | use Schema; |
||
59 | use UnexpectedValueException; |
||
60 | |||
61 | /** |
||
62 | * Class UpgradeDatabase. |
||
63 | * |
||
64 | * Upgrade user database. |
||
65 | * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
||
66 | * @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
||
67 | * |
||
68 | * @codeCoverageIgnore |
||
69 | */ |
||
70 | class UpgradeDatabase extends Command |
||
71 | { |
||
72 | /** |
||
73 | * The console command description. |
||
74 | * |
||
75 | * @var string |
||
76 | */ |
||
77 | protected $description = 'Will run various commands to update database records.'; |
||
78 | /** |
||
79 | * The name and signature of the console command. |
||
80 | * |
||
81 | * @var string |
||
82 | */ |
||
83 | protected $signature = 'firefly:upgrade-database'; |
||
84 | |||
85 | /** |
||
86 | * Execute the console command. |
||
87 | */ |
||
88 | public function handle(): int |
||
89 | { |
||
90 | $this->setTransactionIdentifier(); |
||
91 | $this->updateAccountCurrencies(); |
||
92 | $this->createNewTypes(); |
||
93 | $this->line('Updating currency information..'); |
||
94 | $this->updateTransferCurrencies(); |
||
95 | $this->updateOtherCurrencies(); |
||
96 | $this->line('Done updating currency information..'); |
||
97 | $this->migrateNotes(); |
||
98 | $this->migrateAttachmentData(); |
||
99 | $this->migrateBillsToRules(); |
||
100 | $this->budgetLimitCurrency(); |
||
101 | $this->removeCCLiabilities(); |
||
102 | |||
103 | $this->info('Firefly III database is up to date.'); |
||
104 | |||
105 | return 0; |
||
106 | } |
||
107 | |||
108 | /** |
||
109 | * Since it is one routine these warnings make sense and should be supressed. |
||
110 | * |
||
111 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
||
112 | * @SuppressWarnings(PHPMD.ExcessiveMethodLength) |
||
113 | * @SuppressWarnings(PHPMD.NPathComplexity) |
||
114 | */ |
||
115 | public function migrateBillsToRules(): void |
||
116 | { |
||
117 | foreach (User::get() as $user) { |
||
118 | /** @var Preference $lang */ |
||
119 | $lang = app('preferences')->getForUser($user, 'language', 'en_US'); |
||
120 | $groupName = (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data); |
||
121 | $ruleGroup = $user->ruleGroups()->where('title', $groupName)->first(); |
||
122 | $currencyPreference = app('preferences')->getForUser($user, 'currencyPreference', config('firefly.default_currency', 'EUR')); |
||
123 | |||
124 | if (null === $currencyPreference) { |
||
125 | $this->error('User has no currency preference. Impossible.'); |
||
126 | |||
127 | return; |
||
128 | } |
||
129 | |||
130 | $currency = TransactionCurrency::where('code', $currencyPreference->data)->first(); |
||
131 | if (null === $currency) { |
||
132 | $this->line('Fall back to default currency in migrateBillsToRules().'); |
||
133 | $currency = app('amount')->getDefaultCurrency(); |
||
134 | } |
||
135 | |||
136 | if (null === $ruleGroup) { |
||
137 | $array = RuleGroup::get(['order'])->pluck('order')->toArray(); |
||
138 | $order = \count($array) > 0 ? max($array) + 1 : 1; |
||
139 | $ruleGroup = RuleGroup::create( |
||
140 | [ |
||
141 | 'user_id' => $user->id, |
||
142 | 'title' => (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data), |
||
143 | 'description' => (string)trans('firefly.rulegroup_for_bills_description', [], $lang->data), |
||
144 | 'order' => $order, |
||
145 | 'active' => 1, |
||
146 | ] |
||
147 | ); |
||
148 | } |
||
149 | |||
150 | // loop bills. |
||
151 | $order = 1; |
||
152 | /** @var Collection $collection */ |
||
153 | $collection = $user->bills()->get(); |
||
154 | /** @var Bill $bill */ |
||
155 | foreach ($collection as $bill) { |
||
156 | if ('MIGRATED_TO_RULES' !== $bill->match) { |
||
157 | $rule = Rule::create( |
||
158 | [ |
||
159 | 'user_id' => $user->id, |
||
160 | 'rule_group_id' => $ruleGroup->id, |
||
161 | 'title' => (string)trans('firefly.rule_for_bill_title', ['name' => $bill->name], $lang->data), |
||
162 | 'description' => (string)trans('firefly.rule_for_bill_description', ['name' => $bill->name], $lang->data), |
||
163 | 'order' => $order, |
||
164 | 'active' => $bill->active, |
||
165 | 'stop_processing' => 1, |
||
166 | ] |
||
167 | ); |
||
168 | // add default trigger |
||
169 | RuleTrigger::create( |
||
170 | [ |
||
171 | 'rule_id' => $rule->id, |
||
172 | 'trigger_type' => 'user_action', |
||
173 | 'trigger_value' => 'store-journal', |
||
174 | 'active' => 1, |
||
175 | 'stop_processing' => 0, |
||
176 | 'order' => 1, |
||
177 | ] |
||
178 | ); |
||
179 | // add trigger for description |
||
180 | $match = implode(' ', explode(',', $bill->match)); |
||
181 | RuleTrigger::create( |
||
182 | [ |
||
183 | 'rule_id' => $rule->id, |
||
184 | 'trigger_type' => 'description_contains', |
||
185 | 'trigger_value' => $match, |
||
186 | 'active' => 1, |
||
187 | 'stop_processing' => 0, |
||
188 | 'order' => 2, |
||
189 | ] |
||
190 | ); |
||
191 | if ($bill->amount_max !== $bill->amount_min) { |
||
192 | // add triggers for amounts: |
||
193 | RuleTrigger::create( |
||
194 | [ |
||
195 | 'rule_id' => $rule->id, |
||
196 | 'trigger_type' => 'amount_less', |
||
197 | 'trigger_value' => round($bill->amount_max, $currency->decimal_places), |
||
198 | 'active' => 1, |
||
199 | 'stop_processing' => 0, |
||
200 | 'order' => 3, |
||
201 | ] |
||
202 | ); |
||
203 | RuleTrigger::create( |
||
204 | [ |
||
205 | 'rule_id' => $rule->id, |
||
206 | 'trigger_type' => 'amount_more', |
||
207 | 'trigger_value' => round((float)$bill->amount_min, $currency->decimal_places), |
||
208 | 'active' => 1, |
||
209 | 'stop_processing' => 0, |
||
210 | 'order' => 4, |
||
211 | ] |
||
212 | ); |
||
213 | } |
||
214 | if ($bill->amount_max === $bill->amount_min) { |
||
215 | RuleTrigger::create( |
||
216 | [ |
||
217 | 'rule_id' => $rule->id, |
||
218 | 'trigger_type' => 'amount_exactly', |
||
219 | 'trigger_value' => round((float)$bill->amount_min, $currency->decimal_places), |
||
220 | 'active' => 1, |
||
221 | 'stop_processing' => 0, |
||
222 | 'order' => 3, |
||
223 | ] |
||
224 | ); |
||
225 | } |
||
226 | |||
227 | // create action |
||
228 | RuleAction::create( |
||
229 | [ |
||
230 | 'rule_id' => $rule->id, |
||
231 | 'action_type' => 'link_to_bill', |
||
232 | 'action_value' => $bill->name, |
||
233 | 'order' => 1, |
||
234 | 'active' => 1, |
||
235 | 'stop_processing' => 0, |
||
236 | ] |
||
237 | ); |
||
238 | |||
239 | $order++; |
||
240 | $bill->match = 'MIGRATED_TO_RULES'; |
||
241 | $bill->save(); |
||
242 | $this->line(sprintf('Updated bill #%d ("%s") so it will use rules.', $bill->id, $bill->name)); |
||
243 | } |
||
244 | |||
245 | // give bills a currency when they dont have one. |
||
246 | if (null === $bill->transaction_currency_id) { |
||
247 | $this->line(sprintf('Gave bill #%d ("%s") a currency (%s).', $bill->id, $bill->name, $currency->name)); |
||
248 | $bill->transactionCurrency()->associate($currency); |
||
249 | $bill->save(); |
||
250 | } |
||
251 | } |
||
252 | } |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * This method gives all transactions which are part of a split journal (so more than 2) a sort of "order" so they are easier |
||
257 | * to easier to match to their counterpart. When a journal is split, it has two or three transactions: -3, -4 and -5 for example. |
||
258 | * |
||
259 | * In the database this is reflected as 6 transactions: -3/+3, -4/+4, -5/+5. |
||
260 | * |
||
261 | * When either of these are the same amount, FF3 can't keep them apart: +3/-3, +3/-3, +3/-3. This happens more often than you would |
||
262 | * think. So each set gets a number (1,2,3) to keep them apart. |
||
263 | */ |
||
264 | public function setTransactionIdentifier(): void |
||
265 | { |
||
266 | // if table does not exist, return false |
||
267 | if (!Schema::hasTable('transaction_journals')) { |
||
268 | return; |
||
269 | } |
||
270 | $subQuery = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') |
||
271 | ->whereNull('transaction_journals.deleted_at') |
||
272 | ->whereNull('transactions.deleted_at') |
||
273 | ->groupBy(['transaction_journals.id']) |
||
274 | ->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]); |
||
275 | $result = DB::table(DB::raw('(' . $subQuery->toSql() . ') AS derived')) |
||
276 | ->mergeBindings($subQuery->getQuery()) |
||
277 | ->where('t_count', '>', 2) |
||
278 | ->select(['id', 't_count']); |
||
279 | $journalIds = array_unique($result->pluck('id')->toArray()); |
||
280 | |||
281 | foreach ($journalIds as $journalId) { |
||
282 | $this->updateJournalidentifiers((int)$journalId); |
||
283 | } |
||
284 | |||
285 | } |
||
286 | |||
287 | /** |
||
288 | * Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's forced upon the account. |
||
289 | * |
||
290 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
||
291 | * @SuppressWarnings(PHPMD.ExcessiveMethodLength) |
||
292 | */ |
||
293 | public function updateAccountCurrencies(): void |
||
294 | { |
||
295 | $accounts = Account::leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') |
||
296 | ->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->get(['accounts.*']); |
||
297 | /** @var AccountRepositoryInterface $repository */ |
||
298 | $repository = app(AccountRepositoryInterface::class); |
||
299 | $accounts->each( |
||
300 | function (Account $account) use ($repository) { |
||
301 | $repository->setUser($account->user); |
||
302 | // get users preference, fall back to system pref. |
||
303 | $defaultCurrencyCode = app('preferences')->getForUser($account->user, 'currencyPreference', config('firefly.default_currency', 'EUR'))->data; |
||
304 | $defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first(); |
||
305 | $accountCurrency = (int)$repository->getMetaValue($account, 'currency_id'); |
||
306 | $openingBalance = $account->getOpeningBalance(); |
||
307 | $obCurrency = (int)$openingBalance->transaction_currency_id; |
||
308 | |||
309 | if (null === $defaultCurrency) { |
||
310 | throw new UnexpectedValueException('The default currency is NULL, and this is more or less impossible.'); |
||
311 | } |
||
312 | |||
313 | // both 0? set to default currency: |
||
314 | if (0 === $accountCurrency && 0 === $obCurrency) { |
||
315 | AccountMeta::where('account_id', $account->id)->where('name', 'currency_id')->forceDelete(); |
||
316 | AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]); |
||
317 | $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode)); |
||
318 | |||
319 | return true; |
||
320 | } |
||
321 | |||
322 | // account is set to 0, opening balance is not? |
||
323 | if (0 === $accountCurrency && $obCurrency > 0) { |
||
324 | AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $obCurrency]); |
||
325 | $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode)); |
||
326 | |||
327 | return true; |
||
328 | } |
||
329 | |||
330 | // do not match and opening balance id is not null. |
||
331 | if ($accountCurrency !== $obCurrency && $openingBalance->id > 0) { |
||
332 | // update opening balance: |
||
333 | $openingBalance->transaction_currency_id = $accountCurrency; |
||
334 | $openingBalance->save(); |
||
335 | $this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name)); |
||
336 | |||
337 | return true; |
||
338 | } |
||
339 | |||
340 | return true; |
||
341 | } |
||
342 | ); |
||
343 | |||
344 | } |
||
345 | |||
346 | /** |
||
347 | * This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for |
||
348 | * the accounts they are linked to. |
||
349 | * |
||
350 | * Both source and destination must match the respective currency preference of the related asset account. |
||
351 | * So FF3 must verify all transactions. |
||
352 | * |
||
353 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
||
354 | * @SuppressWarnings(PHPMD.ExcessiveMethodLength) |
||
355 | */ |
||
356 | public function updateOtherCurrencies(): void |
||
357 | { |
||
358 | /** @var CurrencyRepositoryInterface $repository */ |
||
359 | $repository = app(CurrencyRepositoryInterface::class); |
||
360 | /** @var AccountRepositoryInterface $accountRepos */ |
||
361 | $accountRepos = app(AccountRepositoryInterface::class); |
||
362 | $set = TransactionJournal |
||
363 | ::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') |
||
364 | ->whereIn('transaction_types.type', [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE]) |
||
365 | ->get(['transaction_journals.*']); |
||
366 | |||
367 | $set->each( |
||
368 | function (TransactionJournal $journal) use ($repository, $accountRepos) { |
||
369 | // get the transaction with the asset account in it: |
||
370 | /** @var Transaction $transaction */ |
||
371 | $transaction = $journal->transactions() |
||
372 | ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') |
||
373 | ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') |
||
374 | ->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->first(['transactions.*']); |
||
375 | if (null === $transaction) { |
||
376 | return; |
||
377 | } |
||
378 | $accountRepos->setUser($journal->user); |
||
379 | /** @var Account $account */ |
||
380 | $account = $transaction->account; |
||
381 | $currency = $repository->findNull((int)$accountRepos->getMetaValue($account, 'currency_id')); |
||
382 | if (null === $currency) { |
||
383 | return; |
||
384 | } |
||
385 | $transactions = $journal->transactions()->get(); |
||
386 | $transactions->each( |
||
387 | function (Transaction $transaction) use ($currency) { |
||
388 | if (null === $transaction->transaction_currency_id) { |
||
389 | $transaction->transaction_currency_id = $currency->id; |
||
390 | $transaction->save(); |
||
391 | } |
||
392 | |||
393 | // when mismatch in transaction: |
||
394 | if (!((int)$transaction->transaction_currency_id === (int)$currency->id)) { |
||
395 | $transaction->foreign_currency_id = (int)$transaction->transaction_currency_id; |
||
396 | $transaction->foreign_amount = $transaction->amount; |
||
397 | $transaction->transaction_currency_id = $currency->id; |
||
398 | $transaction->save(); |
||
399 | } |
||
400 | } |
||
401 | ); |
||
402 | // also update the journal, of course: |
||
403 | $journal->transaction_currency_id = $currency->id; |
||
404 | $journal->save(); |
||
405 | } |
||
406 | ); |
||
407 | |||
408 | } |
||
409 | |||
410 | /** |
||
411 | * This routine verifies that transfers have the correct currency settings for the accounts they are linked to. |
||
412 | * For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they |
||
413 | * like it or not. Previous routines MUST have set the currency setting for both accounts for this to work. |
||
414 | * |
||
415 | * A transfer always has the |
||
416 | * |
||
417 | * Both source and destination must match the respective currency preference. So FF3 must verify ALL |
||
418 | * transactions. |
||
419 | */ |
||
420 | public function updateTransferCurrencies(): void |
||
421 | { |
||
422 | $set = TransactionJournal |
||
423 | ::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') |
||
424 | ->where('transaction_types.type', TransactionType::TRANSFER) |
||
425 | ->get(['transaction_journals.*']); |
||
426 | |||
427 | $set->each( |
||
428 | function (TransactionJournal $transfer) { |
||
429 | // select all "source" transactions: |
||
430 | /** @var Collection $transactions */ |
||
431 | $transactions = $transfer->transactions()->where('amount', '<', 0)->get(); |
||
432 | $transactions->each( |
||
433 | function (Transaction $transaction) { |
||
434 | $this->updateTransactionCurrency($transaction); |
||
435 | $this->updateJournalCurrency($transaction); |
||
436 | } |
||
437 | ); |
||
438 | } |
||
439 | ); |
||
440 | } |
||
441 | |||
442 | /** |
||
443 | * |
||
444 | */ |
||
445 | private function budgetLimitCurrency(): void |
||
446 | { |
||
447 | $budgetLimits = BudgetLimit::get(); |
||
448 | /** @var BudgetLimit $budgetLimit */ |
||
449 | foreach ($budgetLimits as $budgetLimit) { |
||
450 | if (null === $budgetLimit->transaction_currency_id) { |
||
451 | $budget = $budgetLimit->budget; |
||
452 | if (null !== $budget) { |
||
453 | $user = $budget->user; |
||
454 | if (null !== $user) { |
||
455 | $currency = \Amount::getDefaultCurrencyByUser($user); |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
456 | $budgetLimit->transaction_currency_id = $currency->id; |
||
457 | $budgetLimit->save(); |
||
458 | $this->line( |
||
459 | sprintf('Budget limit #%d (part of budget "%s") now has a currency setting (%s).', $budgetLimit->id, $budget->name, $currency->name) |
||
460 | ); |
||
461 | } |
||
462 | } |
||
463 | } |
||
464 | } |
||
465 | } |
||
466 | |||
467 | private function createNewTypes(): void |
||
468 | { |
||
469 | // create transaction type "Reconciliation". |
||
470 | $type = TransactionType::where('type', TransactionType::RECONCILIATION)->first(); |
||
471 | if (null === $type) { |
||
472 | TransactionType::create(['type' => TransactionType::RECONCILIATION]); |
||
473 | } |
||
474 | $account = AccountType::where('type', AccountType::RECONCILIATION)->first(); |
||
475 | if (null === $account) { |
||
476 | AccountType::create(['type' => AccountType::RECONCILIATION]); |
||
477 | } |
||
478 | } |
||
479 | |||
480 | /** |
||
481 | * Move the description of each attachment (when not NULL) to the notes or to a new note object |
||
482 | * for all attachments. |
||
483 | */ |
||
484 | private function migrateAttachmentData(): void |
||
485 | { |
||
486 | $attachments = Attachment::get(); |
||
487 | |||
488 | /** @var Attachment $att */ |
||
489 | foreach ($attachments as $att) { |
||
490 | |||
491 | // move description: |
||
492 | $description = (string)$att->description; |
||
493 | if (\strlen($description) > 0) { |
||
494 | // find or create note: |
||
495 | $note = $att->notes()->first(); |
||
496 | if (null === $note) { |
||
497 | $note = new Note; |
||
498 | $note->noteable()->associate($att); |
||
499 | } |
||
500 | $note->text = $description; |
||
501 | $note->save(); |
||
502 | |||
503 | // clear description: |
||
504 | $att->description = ''; |
||
505 | $att->save(); |
||
506 | |||
507 | Log::debug(sprintf('Migrated attachment #%s description to note #%d', $att->id, $note->id)); |
||
508 | } |
||
509 | } |
||
510 | } |
||
511 | |||
512 | /** |
||
513 | * Move all the journal_meta notes to their note object counter parts. |
||
514 | */ |
||
515 | private function migrateNotes(): void |
||
516 | { |
||
517 | /** @noinspection PhpUndefinedMethodInspection */ |
||
518 | $set = TransactionJournalMeta::whereName('notes')->get(); |
||
519 | /** @var TransactionJournalMeta $meta */ |
||
520 | foreach ($set as $meta) { |
||
521 | $journal = $meta->transactionJournal; |
||
522 | $note = $journal->notes()->first(); |
||
523 | if (null === $note) { |
||
524 | $note = new Note(); |
||
525 | $note->noteable()->associate($journal); |
||
526 | } |
||
527 | |||
528 | $note->text = $meta->data; |
||
529 | $note->save(); |
||
530 | Log::debug(sprintf('Migrated meta note #%d to Note #%d', $meta->id, $note->id)); |
||
531 | try { |
||
532 | $meta->delete(); |
||
533 | } catch (Exception $e) { |
||
534 | Log::error(sprintf('Could not delete old meta entry #%d: %s', $meta->id, $e->getMessage())); |
||
535 | } |
||
536 | } |
||
537 | } |
||
538 | |||
539 | /** |
||
540 | * |
||
541 | */ |
||
542 | private function removeCCLiabilities(): void |
||
543 | { |
||
544 | $ccType = AccountType::where('type', AccountType::CREDITCARD)->first(); |
||
545 | $debtType =AccountType::where('type', AccountType::DEBT)->first(); |
||
546 | if(null === $ccType || null === $debtType) { |
||
547 | return; |
||
548 | } |
||
549 | /** @var Collection $accounts */ |
||
550 | $accounts = Account::where('account_type_id', $ccType->id)->get(); |
||
551 | foreach($accounts as $account) { |
||
552 | $account->account_type_id = $debtType->id; |
||
553 | $account->save(); |
||
554 | $this->line(sprintf('Converted credit card liability account "%s" (#%d) to generic debt liability.', $account->name, $account->id)); |
||
555 | } |
||
556 | if($accounts->count() > 0) { |
||
557 | $this->info('Credit card liability types are no longer supported and have been converted to generic debts. See: http://bit.ly/FF3-credit-cards'); |
||
558 | } |
||
559 | } |
||
560 | |||
561 | /** |
||
562 | * This method makes sure that the transaction journal uses the currency given in the transaction. |
||
563 | * |
||
564 | * @param Transaction $transaction |
||
565 | */ |
||
566 | private function updateJournalCurrency(Transaction $transaction): void |
||
567 | { |
||
568 | /** @var CurrencyRepositoryInterface $repository */ |
||
569 | $repository = app(CurrencyRepositoryInterface::class); |
||
570 | /** @var AccountRepositoryInterface $accountRepos */ |
||
571 | $accountRepos = app(AccountRepositoryInterface::class); |
||
572 | $accountRepos->setUser($transaction->account->user); |
||
573 | $currency = $repository->findNull((int)$accountRepos->getMetaValue($transaction->account, 'currency_id')); |
||
574 | $journal = $transaction->transactionJournal; |
||
575 | |||
576 | if (null === $currency) { |
||
577 | return; |
||
578 | } |
||
579 | |||
580 | if (!((int)$currency->id === (int)$journal->transaction_currency_id)) { |
||
581 | $this->line( |
||
582 | sprintf( |
||
583 | 'Transfer #%d ("%s") has been updated to use %s instead of %s.', |
||
584 | $journal->id, |
||
585 | $journal->description, |
||
586 | $currency->code, |
||
587 | $journal->transactionCurrency->code |
||
588 | ) |
||
589 | ); |
||
590 | $journal->transaction_currency_id = $currency->id; |
||
591 | $journal->save(); |
||
592 | } |
||
593 | |||
594 | } |
||
595 | |||
596 | /** |
||
597 | * grab all positive transactiosn from this journal that are not deleted. for each one, grab the negative opposing one |
||
598 | * which has 0 as an identifier and give it the same identifier. |
||
599 | * |
||
600 | * @param int $journalId |
||
601 | */ |
||
602 | private function updateJournalidentifiers(int $journalId): void |
||
603 | { |
||
604 | $identifier = 0; |
||
605 | $processed = []; |
||
606 | $transactions = Transaction::where('transaction_journal_id', $journalId)->where('amount', '>', 0)->get(); |
||
607 | /** @var Transaction $transaction */ |
||
608 | foreach ($transactions as $transaction) { |
||
609 | // find opposing: |
||
610 | $amount = bcmul((string)$transaction->amount, '-1'); |
||
611 | |||
612 | try { |
||
613 | /** @var Transaction $opposing */ |
||
614 | $opposing = Transaction::where('transaction_journal_id', $journalId) |
||
615 | ->where('amount', $amount)->where('identifier', '=', 0) |
||
616 | ->whereNotIn('id', $processed) |
||
617 | ->first(); |
||
618 | } catch (QueryException $e) { |
||
619 | Log::error($e->getMessage()); |
||
620 | $this->error('Firefly III could not find the "identifier" field in the "transactions" table.'); |
||
621 | $this->error(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version'))); |
||
622 | $this->error('Please run "php artisan migrate" to add this field to the table.'); |
||
623 | $this->info('Then, run "php artisan firefly:upgrade-database" to try again.'); |
||
624 | |||
625 | return; |
||
626 | } |
||
627 | if (null !== $opposing) { |
||
628 | // give both a new identifier: |
||
629 | $transaction->identifier = $identifier; |
||
630 | $opposing->identifier = $identifier; |
||
631 | $transaction->save(); |
||
632 | $opposing->save(); |
||
633 | $processed[] = $transaction->id; |
||
634 | $processed[] = $opposing->id; |
||
635 | } |
||
636 | ++$identifier; |
||
637 | } |
||
638 | |||
639 | } |
||
640 | |||
641 | /** |
||
642 | * This method makes sure that the tranaction uses the same currency as the source account does. |
||
643 | * If not, the currency is updated to include a reference to its original currency as the "foreign" currency. |
||
644 | * |
||
645 | * The transaction that is sent to this function MUST be the source transaction (amount negative). |
||
646 | * |
||
647 | * Method is long and complex but I'll allow it. https://imgur.com/gallery/dVDJiez |
||
648 | * |
||
649 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
||
650 | * @SuppressWarnings(PHPMD.ExcessiveMethodLength) |
||
651 | * @SuppressWarnings(PHPMD.NPathComplexity) |
||
652 | * |
||
653 | * @param Transaction $transaction |
||
654 | */ |
||
655 | private function updateTransactionCurrency(Transaction $transaction): void |
||
656 | { |
||
657 | /** @var CurrencyRepositoryInterface $repository */ |
||
658 | $repository = app(CurrencyRepositoryInterface::class); |
||
659 | /** @var AccountRepositoryInterface $accountRepos */ |
||
660 | $accountRepos = app(AccountRepositoryInterface::class); |
||
661 | /** @var JournalRepositoryInterface $journalRepos */ |
||
662 | $journalRepos = app(JournalRepositoryInterface::class); |
||
663 | |||
664 | $accountRepos->setUser($transaction->account->user); |
||
665 | $journalRepos->setUser($transaction->account->user); |
||
666 | $currency = $repository->findNull((int)$accountRepos->getMetaValue($transaction->account, 'currency_id')); |
||
667 | |||
668 | if (null === $currency) { |
||
669 | Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $transaction->account->id, $transaction->account->name)); |
||
670 | |||
671 | return; |
||
672 | } |
||
673 | |||
674 | // has no currency ID? Must have, so fill in using account preference: |
||
675 | if (null === $transaction->transaction_currency_id) { |
||
676 | $transaction->transaction_currency_id = (int)$currency->id; |
||
677 | Log::debug(sprintf('Transaction #%d has no currency setting, now set to %s', $transaction->id, $currency->code)); |
||
678 | $transaction->save(); |
||
679 | } |
||
680 | |||
681 | // does not match the source account (see above)? Can be fixed |
||
682 | // when mismatch in transaction and NO foreign amount is set: |
||
683 | if (!((int)$transaction->transaction_currency_id === (int)$currency->id) && null === $transaction->foreign_amount) { |
||
684 | Log::debug( |
||
685 | sprintf( |
||
686 | 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', |
||
687 | $transaction->id, |
||
688 | $transaction->transaction_currency_id, |
||
689 | $currency->id, |
||
690 | $transaction->amount |
||
691 | ) |
||
692 | ); |
||
693 | $transaction->transaction_currency_id = (int)$currency->id; |
||
694 | $transaction->save(); |
||
695 | } |
||
696 | |||
697 | // grab opposing transaction: |
||
698 | /** @var TransactionJournal $journal */ |
||
699 | $journal = $transaction->transactionJournal; |
||
700 | /** @var Transaction $opposing */ |
||
701 | $opposing = $journal->transactions()->where('amount', '>', 0)->where('identifier', $transaction->identifier)->first(); |
||
702 | $opposingCurrency = $repository->findNull((int)$accountRepos->getMetaValue($opposing->account, 'currency_id')); |
||
703 | |||
704 | if (null === $opposingCurrency) { |
||
705 | Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $opposing->account->id, $opposing->account->name)); |
||
706 | |||
707 | return; |
||
708 | } |
||
709 | |||
710 | // if the destination account currency is the same, both foreign_amount and foreign_currency_id must be NULL for both transactions: |
||
711 | if ((int)$opposingCurrency->id === (int)$currency->id) { |
||
712 | // update both transactions to match: |
||
713 | $transaction->foreign_amount = null; |
||
714 | $transaction->foreign_currency_id = null; |
||
715 | $opposing->foreign_amount = null; |
||
716 | $opposing->foreign_currency_id = null; |
||
717 | $opposing->transaction_currency_id = $currency->id; |
||
718 | $transaction->save(); |
||
719 | $opposing->save(); |
||
720 | Log::debug( |
||
721 | sprintf( |
||
722 | 'Currency for account "%s" is %s, and currency for account "%s" is also |
||
723 | %s, so %s #%d (#%d and #%d) has been verified to be to %s exclusively.', |
||
724 | $opposing->account->name, $opposingCurrency->code, |
||
725 | $transaction->account->name, $transaction->transactionCurrency->code, |
||
726 | $journal->transactionType->type, $journal->id, |
||
727 | $transaction->id, $opposing->id, $currency->code |
||
728 | ) |
||
729 | ); |
||
730 | |||
731 | return; |
||
732 | } |
||
733 | // if destination account currency is different, both transactions must have this currency as foreign currency id. |
||
734 | if (!((int)$opposingCurrency->id === (int)$currency->id)) { |
||
735 | $transaction->foreign_currency_id = $opposingCurrency->id; |
||
736 | $opposing->foreign_currency_id = $opposingCurrency->id; |
||
737 | $transaction->save(); |
||
738 | $opposing->save(); |
||
739 | Log::debug(sprintf('Verified foreign currency ID of transaction #%d and #%d', $transaction->id, $opposing->id)); |
||
740 | } |
||
741 | |||
742 | // if foreign amount of one is null and the other is not, use this to restore: |
||
743 | if (null === $transaction->foreign_amount && null !== $opposing->foreign_amount) { |
||
744 | $transaction->foreign_amount = bcmul((string)$opposing->foreign_amount, '-1'); |
||
745 | $transaction->save(); |
||
746 | Log::debug(sprintf('Restored foreign amount of transaction (1) #%d to %s', $transaction->id, $transaction->foreign_amount)); |
||
747 | } |
||
748 | |||
749 | // if foreign amount of one is null and the other is not, use this to restore (other way around) |
||
750 | if (null === $opposing->foreign_amount && null !== $transaction->foreign_amount) { |
||
751 | $opposing->foreign_amount = bcmul((string)$transaction->foreign_amount, '-1'); |
||
752 | $opposing->save(); |
||
753 | Log::debug(sprintf('Restored foreign amount of transaction (2) #%d to %s', $opposing->id, $opposing->foreign_amount)); |
||
754 | } |
||
755 | |||
756 | // when both are zero, try to grab it from journal: |
||
757 | if (null === $opposing->foreign_amount && null === $transaction->foreign_amount) { |
||
758 | $foreignAmount = $journalRepos->getMetaField($journal, 'foreign_amount'); |
||
759 | if (null === $foreignAmount) { |
||
760 | Log::debug(sprintf('Journal #%d has missing foreign currency data, forced to do 1:1 conversion :(.', $transaction->transaction_journal_id)); |
||
761 | $transaction->foreign_amount = bcmul((string)$transaction->amount, '-1'); |
||
762 | $opposing->foreign_amount = bcmul((string)$opposing->amount, '-1'); |
||
763 | $transaction->save(); |
||
764 | $opposing->save(); |
||
765 | |||
766 | return; |
||
767 | } |
||
768 | $foreignPositive = app('steam')->positive((string)$foreignAmount); |
||
769 | Log::debug( |
||
770 | sprintf( |
||
771 | 'Journal #%d has missing foreign currency info, try to restore from meta-data ("%s").', |
||
772 | $transaction->transaction_journal_id, |
||
773 | $foreignAmount |
||
774 | ) |
||
775 | ); |
||
776 | $transaction->foreign_amount = bcmul($foreignPositive, '-1'); |
||
777 | $opposing->foreign_amount = $foreignPositive; |
||
778 | $transaction->save(); |
||
779 | $opposing->save(); |
||
780 | } |
||
781 | |||
782 | } |
||
783 | |||
784 | } |
||
785 |