Make sure the convert controller works again.

This commit is contained in:
James Cole 2019-07-05 19:43:16 +02:00
parent 3c5c14ff5a
commit 7fd3f77c3e
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
17 changed files with 703 additions and 275 deletions

View File

@ -111,7 +111,7 @@ class NetWorth implements NetWorthInterface
// if the account is a credit card, subtract the virtual balance from the balance, // if the account is a credit card, subtract the virtual balance from the balance,
// to better reflect that this is not money that is actually "yours". // to better reflect that this is not money that is actually "yours".
$role = (string)$this->accountRepository->getMetaValue($account, 'accountRole'); $role = (string)$this->accountRepository->getMetaValue($account, 'account_role');
$virtualBalance = (string)$account->virtual_balance; $virtualBalance = (string)$account->virtual_balance;
if ('ccAsset' === $role && '' !== $virtualBalance && (float)$virtualBalance > 0) { if ('ccAsset' === $role && '' !== $virtualBalance && (float)$virtualBalance > 0) {
$balance = bcsub($balance, $virtualBalance); $balance = bcsub($balance, $virtualBalance);

View File

@ -93,6 +93,37 @@ class AutoCompleteController extends Controller
return response()->json($return); return response()->json($return);
} }
/**
* An auto-complete specifically for revenue accounts, used when converting transactions mostly.
* @param Request $request
*
* @return JsonResponse
*/
public function revenueAccounts(Request $request): JsonResponse
{
$search = $request->get('search');
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
// filter the account types:
$allowedAccountTypes = [AccountType::REVENUE];
Log::debug('Now in accounts(). Filtering results.', $allowedAccountTypes);
$return = [];
$result = $repository->searchAccount((string)$search, $allowedAccountTypes);
/** @var Account $account */
foreach ($result as $account) {
$return[] = [
'id' => $account->id,
'name' => $account->name,
'type' => $account->accountType->type,
];
}
return response()->json($return);
}
/** /**
* Searches in the titles of all transaction journals. * Searches in the titles of all transaction journals.
* The result is limited to the top 15 unique results. * The result is limited to the top 15 unique results.

View File

@ -22,17 +22,26 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction; namespace FireflyIII\Http\Controllers\Transaction;
use Carbon\Carbon;
use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Services\Internal\Update\JournalUpdateService;
use FireflyIII\Support\Http\Controllers\ModelInformation; use FireflyIII\Support\Http\Controllers\ModelInformation;
use FireflyIII\Transformers\TransactionGroupTransformer;
use FireflyIII\Validation\AccountValidator;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Log; use Log;
use View; use View;
/** /**
* Class ConvertController. * Class ConvertController.
*/ */
@ -45,6 +54,7 @@ class ConvertController extends Controller
/** /**
* ConvertController constructor. * ConvertController constructor.
* @codeCoverageIgnore
*/ */
public function __construct() public function __construct()
{ {
@ -68,110 +78,271 @@ class ConvertController extends Controller
* Show overview of a to be converted transaction. * Show overview of a to be converted transaction.
* *
* @param TransactionType $destinationType * @param TransactionType $destinationType
* @param TransactionJournal $journal * @param TransactionGroup $group
* *
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
* @throws \Exception
*/ */
public function index(TransactionType $destinationType, TransactionJournal $journal) public function index(TransactionType $destinationType, TransactionGroup $group)
{ {
// @codeCoverageIgnoreStart /** @var TransactionGroupTransformer $transformer */
if ($this->isOpeningBalance($journal)) { $transformer = app(TransactionGroupTransformer::class);
Log::debug('This is an opening balance.');
return $this->redirectToAccount($journal); /** @var TransactionJournal $first */
$first = $group->transactionJournals()->first();
$sourceType = $first->transactionType;
// return to account.
if (!in_array($sourceType->type, [TransactionType::WITHDRAWAL, TransactionType::TRANSFER, TransactionType::DEPOSIT], true)) {
return $this->redirectToAccount($first);
} }
// @codeCoverageIgnoreEnd
$positiveAmount = $this->repository->getJournalTotal($journal); $groupTitle = $group->title ?? $first->description;
$sourceType = $journal->transactionType; $groupArray = $transformer->transformObject($group);
$subTitle = (string)trans('firefly.convert_to_' . $destinationType->type, ['description' => $journal->description]); $subTitle = (string)trans('firefly.convert_to_' . $destinationType->type, ['description' => $groupTitle]);
$subTitleIcon = 'fa-exchange'; $subTitleIcon = 'fa-exchange';
// get a list of asset accounts and liabilities and stuff, in various combinations:
$validDepositSources = $this->getValidDepositSources();
$validWithdrawalDests = $this->getValidWithdrawalDests();
$liabilities = $this->getLiabilities();
$assets = $this->getAssetAccounts();
// old input variables:
$preFilled = [
'source_name' => old('source_name'),
];
if ($sourceType->type === $destinationType->type) { // cannot convert to its own type. if ($sourceType->type === $destinationType->type) { // cannot convert to its own type.
Log::debug('This is already a transaction of the expected type..'); Log::debug('This is already a transaction of the expected type..');
session()->flash('info', (string)trans('firefly.convert_is_already_type_' . $destinationType->type)); session()->flash('info', (string)trans('firefly.convert_is_already_type_' . $destinationType->type));
return redirect(route('transactions.show', [$journal->id])); return redirect(route('transactions.show', [$group->id]));
} }
if ($journal->transactions()->count() > 2) { // cannot convert split.
Log::info('This journal has more than two transactions.');
session()->flash('error', (string)trans('firefly.cannot_convert_split_journal'));
return redirect(route('transactions.show', [$journal->id]));
}
// get source and destination account:
$sourceAccount = $this->repository->getJournalSourceAccounts($journal)->first();
$destinationAccount = $this->repository->getJournalDestinationAccounts($journal)->first();
return view( return view(
'transactions.convert', compact( 'transactions.convert', compact(
'sourceType', 'destinationType', 'journal', 'positiveAmount', 'sourceAccount', 'destinationAccount', 'sourceType', 'sourceType', 'destinationType',
'group', 'groupTitle', 'groupArray', 'assets', 'validDepositSources', 'liabilities',
'validWithdrawalDests', 'preFilled',
'subTitle', 'subTitleIcon' 'subTitle', 'subTitleIcon'
) )
); );
} }
/** /**
* Do the conversion. * Do the conversion.
* *
* @param Request $request * @param Request $request
* @param TransactionType $destinationType * @param TransactionType $destinationType
* @param TransactionJournal $journal * @param TransactionGroup $group
* *
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* *
* @throws FireflyException * @throws FireflyException
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/ */
public function postIndex(Request $request, TransactionType $destinationType, TransactionJournal $journal) public function postIndex(Request $request, TransactionType $destinationType, TransactionGroup $group)
{ {
throw new FireflyException('Needs refactor'); /** @var TransactionJournal $journal */
// @codeCoverageIgnoreStart foreach ($group->transactionJournals as $journal) {
if ($this->isOpeningBalance($journal)) { // catch FF exception.
Log::debug('Journal is opening balance, return to account.'); try {
$this->convertJournal($journal, $destinationType, $request->all());
} catch (FireflyException $e) {
session()->flash('error', $e->getMessage());
return $this->redirectToAccount($journal); return redirect()->route('transactions.convert.index', [strtolower($destinationType->type), $group->id])->withInput();
} }
// @codeCoverageIgnoreEnd
$data = $request->all();
if ($journal->transactionType->type === $destinationType->type) {
Log::info('Journal is already of the desired type.');
session()->flash('error', (string)trans('firefly.convert_is_already_type_' . $destinationType->type));
return redirect(route('transactions.show', [$journal->id]));
} }
session()->flash('success', (string)trans('firefly.converted_to_' . $destinationType->type));
if ($journal->transactions()->count() > 2) {
Log::info('Journal has more than two transactions.');
session()->flash('error', (string)trans('firefly.cannot_convert_split_journal'));
return redirect(route('transactions.show', [$journal->id]));
}
// get the new source and destination account:
$source = $this->getSourceAccount($journal, $destinationType, $data);
$destination = $this->getDestinationAccount($journal, $destinationType, $data);
// update the journal:
$errors = $this->repository->convert($journal, $destinationType, $source, $destination);
if ($errors->count() > 0) {
Log::error('Errors while converting: ', $errors->toArray());
return redirect(route('transactions.convert.index', [strtolower($destinationType->type), $journal->id]))->withErrors($errors)->withInput();
}
// Success? Fire rules!
event(new UpdatedTransactionGroup($group)); event(new UpdatedTransactionGroup($group));
return redirect(route('transactions.show', [$group->id]));
}
session()->flash('success', (string)trans('firefly.converted_to_' . $destinationType->type)); /**
* @return array
* @throws \Exception
*/
private function getAssetAccounts(): array
{
// make repositories
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$accountList = $repository->getActiveAccountsByType([AccountType::ASSET]);
$defaultCurrency = app('amount')->getDefaultCurrency();
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$balance = app('steam')->balance($account, new Carbon);
$currency = $repository->getAccountCurrency($account) ?? $defaultCurrency;
$role = (string)$repository->getMetaValue($account, 'account_role');
if ('' === $role) {
$role = 'no_account_type'; // @codeCoverageIgnore
}
return redirect(route('transactions.show', [$journal->id])); $key = (string)trans('firefly.opt_group_' . $role);
$grouped[$key][$account->id] = $account->name . ' (' . app('amount')->formatAnything($currency, $balance, false) . ')';
}
return $grouped;
}
/**
* @return array
* @throws \Exception
*/
private function getLiabilities(): array
{
// make repositories
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$accountList = $repository->getActiveAccountsByType([AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
$defaultCurrency = app('amount')->getDefaultCurrency();
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$balance = app('steam')->balance($account, new Carbon);
$currency = $repository->getAccountCurrency($account) ?? $defaultCurrency;
$role = 'l_' . $account->accountType->type; // @codeCoverageIgnore
$key = (string)trans('firefly.opt_group_' . $role);
$grouped[$key][$account->id] = $account->name . ' (' . app('amount')->formatAnything($currency, $balance, false) . ')';
}
return $grouped;
}
/**
* @return array
*/
private function getValidDepositSources(): array
{
// make repositories
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$liabilityTypes = [AccountType::MORTGAGE, AccountType::DEBT, AccountType::CREDITCARD, AccountType::LOAN];
$accountList = $repository
->getActiveAccountsByType([AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string)$repository->getMetaValue($account, 'account_role');
$name = $account->name;
if ('' === $role) {
$role = 'no_account_type'; // @codeCoverageIgnore
}
// maybe it's a liability thing:
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = 'l_' . $account->accountType->type; // @codeCoverageIgnore
}
if (AccountType::CASH === $account->accountType->type) {
$role = 'cash_account';
$name = sprintf('(%s)', trans('firefly.cash'));
}
if (AccountType::REVENUE === $account->accountType->type) {
$role = 'revenue_account';
}
$key = (string)trans('firefly.opt_group_' . $role);
$grouped[$key][$account->id] = $name;
}
return $grouped;
}
/**
* @return array
*/
private function getValidWithdrawalDests(): array
{
// make repositories
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$liabilityTypes = [AccountType::MORTGAGE, AccountType::DEBT, AccountType::CREDITCARD, AccountType::LOAN];
$accountList = $repository
->getActiveAccountsByType([AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string)$repository->getMetaValue($account, 'account_role');
$name = $account->name;
if ('' === $role) {
$role = 'no_account_type'; // @codeCoverageIgnore
}
// maybe it's a liability thing:
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = 'l_' . $account->accountType->type; // @codeCoverageIgnore
}
if (AccountType::CASH === $account->accountType->type) {
$role = 'cash_account';
$name = sprintf('(%s)', trans('firefly.cash'));
}
if (AccountType::EXPENSE === $account->accountType->type) {
$role = 'expense_account';
}
$key = (string)trans('firefly.opt_group_' . $role);
$grouped[$key][$account->id] = $name;
}
return $grouped;
}
/**
* @param TransactionJournal $journal
* @param TransactionType $transactionType
* @param array $data
* @return TransactionJournal
* @throws FireflyException
*/
private function convertJournal(TransactionJournal $journal, TransactionType $transactionType, array $data): TransactionJournal
{
$sourceType = $journal->transactionType->type;
// make a switch based on original + dest type.
/** @var AccountValidator $validator */
$validator = app(AccountValidator::class);
$validator->setUser(auth()->user());
$validator->setTransactionType($transactionType->type);
$sourceId = $data['source_id'][$journal->id] ?? null;
$sourceName = $data['source_name'][$journal->id] ?? null;
$destinationId = $data['destination_id'][$journal->id] ?? null;
$destinationName = $data['destination_name'][$journal->id] ?? null;
// double check its not an empty string.
$sourceId = '' === $sourceId || null === $sourceId ? null : (int)$sourceId;
$sourceName = '' === $sourceName ? null : $sourceName;
$destinationId = '' === $destinationId || null === $destinationId ? null : (int)$destinationId;
$destinationName = '' === $destinationName ? null : $destinationName;
$validSource = $validator->validateSource($sourceId, $sourceName);
$validDestination = $validator->validateDestination($destinationId, $destinationName);
if (false === $validSource) {
throw new FireflyException(sprintf(trans('firefly.convert_invalid_source'), $journal->id));
}
if (false === $validDestination) {
throw new FireflyException(sprintf(trans('firefly.convert_invalid_destination'), $journal->id));
}
$update = [
'source_id' => $sourceId,
'source_name' => $sourceName,
'destination_id' => $destinationId,
'destination_name' => $destinationName,
'type' => $transactionType->type,
];
/** @var JournalUpdateService $service */
$service = app(JournalUpdateService::class);
$service->setTransactionJournal($journal);
$service->setData($update);
$service->update();
$journal->refresh();
return $journal;
} }
} }

View File

@ -78,7 +78,6 @@ class IndexController extends Controller
$types = config('firefly.transactionTypesByType.' . $objectType); $types = config('firefly.transactionTypesByType.' . $objectType);
$page = (int)$request->get('page'); $page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data; $pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
$pageSize =3;
if (null === $start) { if (null === $start) {
$start = session('start'); $start = session('start');
$end = session('end'); $end = session('end');

View File

@ -103,7 +103,7 @@ class AccountFormRequest extends Request
'account_number' => 'between:1,255|uniqueAccountNumberForUser|nullable', 'account_number' => 'between:1,255|uniqueAccountNumberForUser|nullable',
'account_role' => 'in:' . $accountRoles, 'account_role' => 'in:' . $accountRoles,
'active' => 'boolean', 'active' => 'boolean',
'ccType' => 'in:' . $ccPaymentTypes, 'cc_type' => 'in:' . $ccPaymentTypes,
'cc_monthly_payment_date' => 'date', 'cc_monthly_payment_date' => 'date',
'amount_currency_id_opening_balance' => 'exists:transaction_currencies,id', 'amount_currency_id_opening_balance' => 'exists:transaction_currencies,id',
'amount_currency_id_virtual_balance' => 'exists:transaction_currencies,id', 'amount_currency_id_virtual_balance' => 'exists:transaction_currencies,id',

View File

@ -268,7 +268,7 @@ class AccountRepository implements AccountRepositoryInterface
/** @var Collection $result */ /** @var Collection $result */
$query = $this->user->accounts()->with( $query = $this->user->accounts()->with(
['accountmeta' => function (HasMany $query) { ['accountmeta' => function (HasMany $query) {
$query->where('name', 'accountRole'); $query->where('name', 'account_role');
}] }]
); );
if (count($types) > 0) { if (count($types) > 0) {

View File

@ -75,7 +75,7 @@ class ExpandedForm
$balance = app('steam')->balance($account, new Carbon); $balance = app('steam')->balance($account, new Carbon);
$currencyId = (int)$repository->getMetaValue($account, 'currency_id'); $currencyId = (int)$repository->getMetaValue($account, 'currency_id');
$currency = $currencyRepos->findNull($currencyId); $currency = $currencyRepos->findNull($currencyId);
$role = $repository->getMetaValue($account, 'accountRole'); $role = $repository->getMetaValue($account, 'account_role');
if ('' === $role) { if ('' === $role) {
$role = 'no_account_type'; // @codeCoverageIgnore $role = 'no_account_type'; // @codeCoverageIgnore
} }
@ -321,7 +321,7 @@ class ExpandedForm
// group accounts: // group accounts:
/** @var Account $account */ /** @var Account $account */
foreach ($assetAccounts as $account) { foreach ($assetAccounts as $account) {
$role = $repository->getMetaValue($account, 'accountRole'); $role = $repository->getMetaValue($account, 'account_role');
if (null === $role) { if (null === $role) {
$role = 'no_account_type'; // @codeCoverageIgnore $role = 'no_account_type'; // @codeCoverageIgnore
} }
@ -364,7 +364,7 @@ class ExpandedForm
$balance = app('steam')->balance($account, new Carbon); $balance = app('steam')->balance($account, new Carbon);
$currencyId = (int)$repository->getMetaValue($account, 'currency_id'); $currencyId = (int)$repository->getMetaValue($account, 'currency_id');
$currency = $currencyRepos->findNull($currencyId); $currency = $currencyRepos->findNull($currencyId);
$role = (string)$repository->getMetaValue($account, 'accountRole'); $role = (string)$repository->getMetaValue($account, 'account_role');
if ('' === $role) { if ('' === $role) {
$role = 'no_account_type'; // @codeCoverageIgnore $role = 'no_account_type'; // @codeCoverageIgnore
} }
@ -618,7 +618,7 @@ class ExpandedForm
$balance = app('steam')->balance($account, new Carbon); $balance = app('steam')->balance($account, new Carbon);
$currencyId = (int)$repository->getMetaValue($account, 'currency_id'); $currencyId = (int)$repository->getMetaValue($account, 'currency_id');
$currency = $currencyRepos->findNull($currencyId); $currency = $currencyRepos->findNull($currencyId);
$role = (string)$repository->getMetaValue($account, 'accountRole'); $role = (string)$repository->getMetaValue($account, 'account_role'); // TODO bad form for currency
if ('' === $role) { if ('' === $role) {
$role = 'no_account_type'; // @codeCoverageIgnore $role = 'no_account_type'; // @codeCoverageIgnore
} }

View File

@ -137,7 +137,7 @@ class AccountTransformer extends AbstractTransformer
*/ */
private function getAccountRole(Account $account, string $accountType): ?string private function getAccountRole(Account $account, string $accountType): ?string
{ {
$accountRole = $this->repository->getMetaValue($account, 'accountRole'); $accountRole = $this->repository->getMetaValue($account, 'account_role');
if ('asset' !== $accountType || '' === (string)$accountRole) { if ('asset' !== $accountType || '' === (string)$accountRole) {
$accountRole = null; $accountRole = null;
} }
@ -157,8 +157,8 @@ class AccountTransformer extends AbstractTransformer
$monthlyPaymentDate = null; $monthlyPaymentDate = null;
$creditCardType = null; $creditCardType = null;
if ('ccAsset' === $accountRole && 'asset' === $accountType) { if ('ccAsset' === $accountRole && 'asset' === $accountType) {
$creditCardType = $this->repository->getMetaValue($account, 'ccType'); $creditCardType = $this->repository->getMetaValue($account, 'cc_type');
$monthlyPaymentDate = $this->repository->getMetaValue($account, 'ccMonthlyPaymentDate'); $monthlyPaymentDate = $this->repository->getMetaValue($account, 'cc_monthly_payment_date');
} }
return [$creditCardType, $monthlyPaymentDate]; return [$creditCardType, $monthlyPaymentDate];

View File

@ -127,9 +127,6 @@ class AccountValidator
case TransactionType::RECONCILIATION: case TransactionType::RECONCILIATION:
$result = $this->validateReconciliationDestination($destinationId); $result = $this->validateReconciliationDestination($destinationId);
break; break;
//case TransactionType::OPENING_BALANCE:
//case TransactionType::RECONCILIATION:
// die(sprintf('Cannot handle type "%s"', $this->transactionType));
} }
return $result; return $result;
@ -479,8 +476,8 @@ class AccountValidator
return false; return false;
} }
$this->destination = $search; $this->destination = $search;
// must not be the same as the source account
return true; return !(null !== $this->source && $this->source->id === $this->destination->id);
} }
/** /**

View File

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Validation; namespace FireflyIII\Validation;
use Carbon\Carbon;
use Config; use Config;
use DB; use DB;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;

View File

@ -28,7 +28,35 @@ $(document).ready(function () {
* Set the auto-complete JSON things. * Set the auto-complete JSON things.
*/ */
function setAutocompletes() { function setAutocompletes() {
initRevenueACField('source_account_revenue'); //initRevenueACField('source_account_revenue');
initExpenseACField('destination_account_expense'); //initExpenseACField('destination_account_expense');
makeRevenueAC();
}
function makeRevenueAC() {
var sourceNames = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
prefetch: {
url: 'json/revenue-accounts?uid=' + uid,
filter: function (list) {
return $.map(list, function (object) {
return {name: object.name};
});
}
},
remote: {
url: 'json/revenue-accounts?search=%QUERY&uid=' + uid,
wildcard: '%QUERY',
filter: function (list) {
return $.map(list, function (object) {
return {name: object.name};
});
}
}
});
sourceNames.initialize();
$('.input-revenue').typeahead({hint: true, highlight: true,}, {source: sourceNames, displayKey: 'name', autoSelect: false});
} }

View File

@ -864,6 +864,7 @@ return [
'opt_group_expense_account' => 'Expense accounts', 'opt_group_expense_account' => 'Expense accounts',
'opt_group_revenue_account' => 'Revenue accounts', 'opt_group_revenue_account' => 'Revenue accounts',
'opt_group_l_Loan' => 'Liability: Loan', 'opt_group_l_Loan' => 'Liability: Loan',
'opt_group_cash_account' => 'Cash account',
'opt_group_l_Debt' => 'Liability: Debt', 'opt_group_l_Debt' => 'Liability: Debt',
'opt_group_l_Mortgage' => 'Liability: Mortgage', 'opt_group_l_Mortgage' => 'Liability: Mortgage',
'opt_group_l_Credit card' => 'Liability: Credit card', 'opt_group_l_Credit card' => 'Liability: Credit card',
@ -1258,12 +1259,11 @@ return [
'split_this_withdrawal' => 'Split this withdrawal', 'split_this_withdrawal' => 'Split this withdrawal',
'split_this_deposit' => 'Split this deposit', 'split_this_deposit' => 'Split this deposit',
'split_this_transfer' => 'Split this transfer', 'split_this_transfer' => 'Split this transfer',
'cannot_edit_multiple_source' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple source accounts.',
'cannot_edit_multiple_dest' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple destination accounts.',
'cannot_edit_reconciled' => 'You cannot edit transaction #:id with description ":description" because it has been marked as reconciled.',
'cannot_edit_opening_balance' => 'You cannot edit the opening balance of an account.', 'cannot_edit_opening_balance' => 'You cannot edit the opening balance of an account.',
'no_edit_multiple_left' => 'You have selected no valid transactions to edit.', 'no_edit_multiple_left' => 'You have selected no valid transactions to edit.',
'cannot_convert_split_journal' => 'Cannot convert a split transaction', 'breadcrumb_convert_group' => 'Convert transaction',
'convert_invalid_source' => 'Source information is invalid for transaction #%d.',
'convert_invalid_destination' => 'Destination information is invalid for transaction #%d.',
// Import page (general strings only) // Import page (general strings only)
'import_index_title' => 'Import transactions into Firefly III', 'import_index_title' => 'Import transactions into Firefly III',

View File

@ -1,29 +1,292 @@
{% extends "./layout/default" %} {% extends "./layout/default" %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ Breadcrumbs.render(Route.getCurrentRoute.getName, destinationType, journal) }} {{ Breadcrumbs.render(Route.getCurrentRoute.getName, group, groupTitle) }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<form method="POST" action="{{ route('transactions.convert.index.post', [destinationType.type|lower, journal.id]) }}" accept-charset="UTF-8" <form method="POST" action="{{ route('transactions.convert.index.post', [destinationType.type|lower, group.id]) }}" accept-charset="UTF-8"
class="form-horizontal" id="store" class="form-horizontal"
enctype="multipart/form-data"> enctype="multipart/form-data">
<input name="_token" type="hidden" value="{{ csrf_token() }}"> <input name="_token" type="hidden" value="{{ csrf_token() }}">
<div class="row"> <div class="row">
<div class="col-lg-10 col-md-12 col-sm-12"> <div class="col-lg-8 col-lg-offset-2 col-md-12 col-sm-12">
<div class="box box-primary"> <div class="box box-primary">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">{{ ('convert_options_'~sourceType.type~destinationType.type)|_ }}</h3> <h3 class="box-title">{{ ('convert_options_'~sourceType.type~destinationType.type)|_ }}</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
{{ ExpandedForm.staticText('type', sourceType.type|_) }}
{{ ExpandedForm.staticText('description', '<a href="'~route('transactions.show', journal.id)~'">'~journal.description~'</a>') }}
{{ ExpandedForm.staticText('date', journal.date.formatLocalized(monthAndDayFormat)) }}
<p>
{# ONE: WITHDRAWAL TO DEPOSIT #}
{% if sourceType.type == 'Withdrawal' and destinationType.type == 'Deposit' %}
When converting from a withdrawal to a deposit, the money will be deposited into the
displayed destination account(s), instead of being withdrawn from them. To complete the conversion,
please set the new source account(s) below.
{% endif %}
{# TWO: WITHDRAWAL TO TRANSFER #}
{% if sourceType.type == 'Withdrawal' and destinationType.type == 'Transfer' %}
When converting a withdrawal into a transfer, the money will be transferred away from the source
account(s) into other asset or liability account(s) instead of being spent on the original
expense accounts. To complete the conversion, please select new destination account(s).
{% endif %}
{# THREE: DEPOSIT TO WITHDRAWAL #}
{% if sourceType.type == 'Deposit' and destinationType.type == 'Withdrawal' %}
When converting a deposit into a withdrawal, the money will be withdrawn from the
displayed source account(s), instead of being deposited into them. To complete the conversion,
please select new destination accounts.
{% endif %}
{# FOUR: DEPOSIT TO TRANSFER#}
{% if sourceType.type == 'Deposit' and destinationType.type == 'Transfer' %}
When you convert a deposit into a transfer, the money will be deposited into
the listed destination account(s) from any of your asset or liability account(s).
Please select the new source account(s) to complete the conversion.
{% endif %}
{# FIVE: TRANSFER TO WITHDRAWAL #}
{% if sourceType.type == 'Transfer' and destinationType.type == 'Withdrawal' %}
When you convert a transfer into a withdrawal, the money will be spent
on the destination account(s) you set here, instead of being transferred away.
Please select the new destination account(s) to complete the conversion.
{% endif %}
{# SIX: TRANSFER TO DEPOSIT #}
{% if sourceType.type == 'Transfer' and destinationType.type == 'Deposit' %}
When you convert a transfer into a deposit, the money will be deposited
into the destination account(s) you see here, instead of being transferred into them.
Please select the new source account(s) to complete the conversion.
{% endif %}
</p>
<table class="table">
<tr>
<th style="width:10%;">ID</th>
<th style="width:25%;">Description</th>
<th style="width:25%;">Source account</th>
<th style="width:25%;">Destination account</th>
<th style="width:15%;">Amount</th>
</tr>
{% for transaction in groupArray.transactions %}
<tr>
<td>#{{ transaction.transaction_journal_id }}</td>
<td>{{ transaction.description }}</td>
<td>
{# ONE: WITHDRAWAL TO DEPOSIT #}
{% if sourceType.type == 'Withdrawal' and destinationType.type == 'Deposit' %}
{# NEW DESTINATION = Asset, SOURCE MUST BE [Revenue, Cash, Loan, Debt, Mortgage] #}
{% if
transaction.source_type == 'Asset account' %}
{{ Form.select('source_id['~transaction.transaction_journal_id~']', validDepositSources, null, {class: 'form-control'}) }}
{% endif %}
{# NEW DESTINATION = [Loan, Debt, Mortgage], SOURCE MUST BE [Revenue] #}
{% if
transaction.source_type == 'Loan' or
transaction.source_type == 'Debt' or
transaction.source_type == 'Mortgage' %}
<input
autocomplete="off"
placeholder="Source account"
name="source_name[{{ transaction.transaction_journal_id }}]"
type="text"
value="{% if transaction.destination_type != "Cash account" %}{{ preFilled.source_name[transaction.transaction_journal_id]|default(transaction.destination_name) }}{% endif %}"
class="form-control tt-input input-revenue"
spellcheck="false" dir="auto">
{% endif %}
{% endif %}
{# TWO: WITHDRAWAL TO TRANSFER #}
{% if sourceType.type == 'Withdrawal' and destinationType.type == 'Transfer' %}
<a href="{{ route('accounts.show', [transaction.source_id]) }}"
title="{{ transaction.source_iban|default(transaction.source_name) }}">{{ transaction.source_name }}</a>
{# hide source in hidden input #}
<input type="hidden" name="source_id[{{ transaction.transaction_journal_id }}]" value="{{ transaction.source_id }}">
{% endif %}
{# THREE: DEPOSIT TO WITHDRAWAL #}
{% if sourceType.type == 'Deposit' and destinationType.type == 'Withdrawal' %}
<a href="{{ route('accounts.show', [transaction.destination_id]) }}"
title="{{ transaction.destination_iban|default(transaction.destination_name) }}">{{ transaction.destination_name }}</a>
{# hide new source in hidden input #}
<input type="hidden" name="source_id[{{ transaction.transaction_journal_id }}]" value="{{ transaction.destination_id }}">
{% endif %}
{# FOUR: DEPOSIT TO TRANSFER#}
{% if sourceType.type == 'Deposit' and destinationType.type == 'Transfer' %}
{# if new destination is asset, then asset#}
{% if transaction.destination_type == 'Asset account' %}
{{ Form.select('source_id['~transaction.transaction_journal_id~']', assets,null, {class: 'form-control'}) }}
{% endif %}
{% if transaction.destination_type == 'Loan' or
transaction.destination_type == 'Debt' or
transaction.destination_type == 'Mortgage' %}
{{ Form.select('source_id['~transaction.transaction_journal_id~']', liabilities,null, {class: 'form-control'}) }}
{% endif %}
{# if new destination liability, then liability.#}
{% endif %}
{# FIVE: TRANSFER TO WITHDRAWAL #}
{% if sourceType.type == 'Transfer' and destinationType.type == 'Withdrawal' %}
<a href="{{ route('accounts.show', [transaction.source_id]) }}"
title="{{ transaction.source_iban|default(transaction.source_name) }}">{{ transaction.source_name }}</a>
{# hide source in hidden input #}
<input type="hidden" name="source_id[{{ transaction.transaction_journal_id }}]" value="{{ transaction.source_id }}">
{% endif %}
{# SIX: TRANSFER TO DEPOSIT #}
{% if sourceType.type == 'Transfer' and destinationType.type == 'Deposit' %}
{# NEW DESTINATION = Asset, SOURCE MUST BE [Revenue, Cash, Loan, Debt, Mortgage] #}
{% if
transaction.source_type == 'Asset account' %}
{{ Form.select('source_id['~transaction.transaction_journal_id~']', validDepositSources, null, {class: 'form-control'}) }}
{% endif %}
{# NEW DESTINATION = [Debt, Mortgage, Load], SOURCE MUST BE [Revenue] #}
{% if
transaction.source_type == 'Loan' or
transaction.source_type == 'Debt' or
transaction.source_type == 'Mortgage' %}
<input
autocomplete="off"
placeholder="Source account"
name="source_name[{{ transaction.transaction_journal_id }}]"
type="text"
value="{% if transaction.destination_type != "Cash account" %}{{ transaction.source_name }}{% endif %}"
class="form-control tt-input"
spellcheck="false" dir="auto">
{% endif %}
{% endif %}
</td>
<td>
{# ONE: WITHDRAWAL TO DEPOSIT #}
{% if sourceType.type == 'Withdrawal' and destinationType.type == 'Deposit' %}
<a href="{{ route('accounts.show', [transaction.source_id]) }}"
title="{{ transaction.source_iban|default(transaction.source_name) }}">{{ transaction.source_name }}</a>
{# hide destination in hidden input #}
<input type="hidden" name="destination_id[{{ transaction.transaction_journal_id }}]" value="{{ transaction.source_id }}">
{% endif %}
{# TWO: WITHDRAWAL TO TRANSFER #}
{% if sourceType.type == 'Withdrawal' and destinationType.type == 'Transfer' %}
{#if the source is a liability, destination must also be a liability.#}
{% if
transaction.source_type == 'Loan' or
transaction.source_type == 'Debt' or
transaction.source_type == 'Mortgage' %}
{{ Form.select('destination_id['~transaction.transaction_journal_id~']', liabilities, null, {class: 'form-control'}) }}
{% endif %}
{# if the source is an asset, destination can only be an asset. #}
{% if transaction.source_type == 'Asset account' %}
{{ Form.select('destination_id['~transaction.transaction_journal_id~']', assets, null, {class: 'form-control'}) }}
{% endif %}
{% endif %}
{# THREE: DEPOSIT TO WITHDRAWAL #}
{% if sourceType.type == 'Deposit' and destinationType.type == 'Withdrawal' %}
{# if new source is Asset, destination must be [Expense, Loan, Debt or Mortgage] #}
{% if transaction.destination_type == 'Asset account' %}
{{ Form.select('destination_id['~transaction.transaction_journal_id~']', validWithdrawalDests, null, {class: 'form-control'}) }}
{% endif %}
{% if transaction.destination_type == 'Loan' or
transaction.destination_type == 'Debt' or
transaction.destination_type == 'Mortgage' %}
{# if new source is Liability, destination must be expense account. #}
{# hier ben je. #}
<input
autocomplete="off"
placeholder="Destination account"
name="destination_name[{{ transaction.transaction_journal_id }}]"
type="text"
value="{% if transaction.source_type != "Cash account" %}{{ transaction.source_name }}{% endif %}"
class="form-control tt-input"
spellcheck="false" dir="auto">
{% endif %}
{% endif %}
{# FOUR: DEPOSIT TO TRANSFER#}
{% if sourceType.type == 'Deposit' and destinationType.type == 'Transfer' %}
<a href="{{ route('accounts.show', [transaction.destination_id]) }}"
title="{{ transaction.destination_iban|default(transaction.destination_name) }}">{{ transaction.destination_name }}</a>
{# hide destination in hidden input #}
<input type="hidden" name="destination_id[{{ transaction.transaction_journal_id }}]" value="{{ transaction.destination_id }}">
{% endif %}
{# FIVE: TRANSFER TO WITHDRAWAL #}
{% if sourceType.type == 'Transfer' and destinationType.type == 'Withdrawal' %}
{% if transaction.source_type == 'Asset account' %}
{{ Form.select('destination_id['~transaction.transaction_journal_id~']', validWithdrawalDests, null, {class: 'form-control'}) }}
{% endif %}
{% if transaction.source_type == 'Loan' or
transaction.source_type == 'Debt' or
transaction.source_type == 'Mortgage' %}
<input
autocomplete="off"
placeholder="Destination account"
name="destination_name[{{ transaction.transaction_journal_id }}]"
type="text"
value="{% if transaction.source_type != "Cash account" %}{{ transaction.destination_name }}{% endif %}"
class="form-control tt-input"
spellcheck="false" dir="auto">
{% endif %}
{% endif %}
{# SIX: TRANSFER TO DEPOSIT #}
{% if sourceType.type == 'Transfer' and destinationType.type == 'Deposit' %}
<a href="{{ route('accounts.show', [transaction.destination_id]) }}"
title="{{ transaction.destination_iban|default(transaction.destination_name) }}">{{ transaction.destination_name }}</a>
{# hide destination in hidden input #}
<input type="hidden" name="destination_id[{{ transaction.transaction_journal_id }}]" value="{{ transaction.destination_id }}">
{% endif %}
</td>
<td>
{% if transaction.transaction_type_type == 'Deposit' %}
{{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_symbol_decimal_places) }}
{% if null != transaction.foreign_amount %}
({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_symbol_decimal_places) }})
{% endif %}
{% elseif transaction.transaction_type_type == 'Transfer' %}
<span class="text-info">{{ formatAmountBySymbol(transaction.amount*-1, transaction.currency_symbol, transaction.currency_symbol_decimal_places, false) }}
{% if null != transaction.foreign_amount %}
({{ formatAmountBySymbol(transaction.foreign_amount*-1, transaction.foreign_currency_symbol, transaction.foreign_currency_symbol_decimal_places, false) }})
{% endif %}</span>
{% else %}
{{ formatAmountBySymbol(transaction.amount, transaction.currency_symbol, transaction.currency_symbol_decimal_places) }}
{% if null != transaction.foreign_amount %}
({{ formatAmountBySymbol(transaction.foreign_amount, transaction.foreign_currency_symbol, transaction.foreign_currency_symbol_decimal_places) }})
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{#
{{ ExpandedForm.staticText('description', '<a href="'~route('transactions.show', group.id)~'">'~journal.description~'</a>') }}
{{ ExpandedForm.staticText('type', sourceType.type|_) }}
{{ ExpandedForm.staticText('date', journal.date.formatLocalized(monthAndDayFormat)) }}
#}
{# in case of withdrawal #} {# in case of withdrawal #}
{% if sourceType.type == "Withdrawal" %} {#
{% if journalType.type == "Withdrawal" %}
{{ ExpandedForm.staticText('source_account_asset', '<a href="'~route('accounts.show',[sourceAccount.id])~'">'~sourceAccount.name~'</a>') }} {{ ExpandedForm.staticText('source_account_asset', '<a href="'~route('accounts.show',[sourceAccount.id])~'">'~sourceAccount.name~'</a>') }}
{# if destination is cash, show (cash) #} <!-- if destination is cash, show (cash) -->
{% if destinationAccount.accountType.type == "Cash account" %} {% if destinationAccount.accountType.type == "Cash account" %}
{{ ExpandedForm.staticText('destination_account_expense', '<span class="text-success">(cash)</a>') }} {{ ExpandedForm.staticText('destination_account_expense', '<span class="text-success">(cash)</a>') }}
{% else %} {% else %}
@ -31,10 +294,11 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
#}
{# in case of deposit #} {# in case of deposit #}
{% if sourceType.type == "Deposit" %} {#
{# if source is cash, show (cash) #} {% if journalType.type == "Deposit" %}
<!-- if source is cash, show (cash) -->
{% if sourceAccount.accountType.type == "Cash account" %} {% if sourceAccount.accountType.type == "Cash account" %}
{{ ExpandedForm.staticText('source_account_revenue', '<span class="text-success">(cash)</a>') }} {{ ExpandedForm.staticText('source_account_revenue', '<span class="text-success">(cash)</a>') }}
{% else %} {% else %}
@ -42,15 +306,22 @@
{% endif %} {% endif %}
{{ ExpandedForm.staticText('destination_account_asset', '<a href="'~route('accounts.show',[destinationAccount.id])~'">'~destinationAccount.name~'</a>') }} {{ ExpandedForm.staticText('destination_account_asset', '<a href="'~route('accounts.show',[destinationAccount.id])~'">'~destinationAccount.name~'</a>') }}
{% endif %} {% endif %}
#}
{# in case of transfer #} {# in case of transfer #}
{% if sourceType.type == "Transfer" %} {#
{% if journalType.type == "Transfer" %}
{{ ExpandedForm.staticText('source_account_asset', '<a href="'~route('accounts.show',[sourceAccount.id])~'">'~sourceAccount.name~'</a>') }} {{ ExpandedForm.staticText('source_account_asset', '<a href="'~route('accounts.show',[sourceAccount.id])~'">'~sourceAccount.name~'</a>') }}
{{ ExpandedForm.staticText('destination_account_asset', '<a href="'~route('accounts.show',[destinationAccount.id])~'">'~destinationAccount.name~'</a>') }} {{ ExpandedForm.staticText('destination_account_asset', '<a href="'~route('accounts.show',[destinationAccount.id])~'">'~destinationAccount.name~'</a>') }}
{% endif %} {% endif %}
#}
{# ONE #} {# ONE #}
{% if sourceType.type == 'Withdrawal' and destinationType.type == 'Deposit' %} {#
{% if journalType.type == 'Withdrawal' and destinationType.type == 'Deposit' %}
ONE
{% endif %}
{% if journalType.type == 'Withdrawal' and destinationType.type == 'Deposit' %}
<p><em> <p><em>
{{ trans('firefly.convert_explanation_withdrawal_deposit', {{ trans('firefly.convert_explanation_withdrawal_deposit',
{ {
@ -72,9 +343,10 @@
{{ ExpandedForm.text('source_account_revenue', destinationAccount.name) }} {{ ExpandedForm.text('source_account_revenue', destinationAccount.name) }}
{% endif %} {% endif %}
{% endif %} {% endif %}
#}
{# TWO #} {# TWO #}
{% if sourceType.type == 'Withdrawal' and destinationType.type == 'Transfer' %} {#
{% if journalType.type == 'Withdrawal' and destinationType.type == 'Transfer' %}
<p><em> <p><em>
{{ trans('firefly.convert_explanation_withdrawal_transfer', {{ trans('firefly.convert_explanation_withdrawal_transfer',
{ {
@ -95,9 +367,10 @@
{{ ExpandedForm.activeLongAccountList('destination_account_asset', null) }} {{ ExpandedForm.activeLongAccountList('destination_account_asset', null) }}
{% endif %} {% endif %}
#}
{# THREE #} {# THREE #}
{% if sourceType.type == 'Deposit' and destinationType.type == 'Withdrawal' %} {#
{% if journalType.type == 'Deposit' and destinationType.type == 'Withdrawal' %}
<p> <p>
<em> <em>
{{ trans('firefly.convert_explanation_deposit_withdrawal', {{ trans('firefly.convert_explanation_deposit_withdrawal',
@ -123,9 +396,10 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
#}
{# FOUR #} {# FOUR #}
{% if sourceType.type == 'Deposit' and destinationType.type == 'Transfer' %} {#
{% if journalType.type == 'Deposit' and destinationType.type == 'Transfer' %}
<p> <p>
<em> <em>
@ -147,9 +421,10 @@
</p> </p>
{{ ExpandedForm.activeLongAccountList('source_account_asset', null) }} {{ ExpandedForm.activeLongAccountList('source_account_asset', null) }}
{% endif %} {% endif %}
#}
{# FIVE #} {# FIVE #}
{% if sourceType.type == 'Transfer' and destinationType.type == 'Withdrawal' %} {#
{% if journalType.type == 'Transfer' and destinationType.type == 'Withdrawal' %}
<p> <p>
<em> <em>
@ -173,9 +448,10 @@
{{ ExpandedForm.text('destination_account_expense', destinationAccount.name) }} {{ ExpandedForm.text('destination_account_expense', destinationAccount.name) }}
{% endif %} {% endif %}
#}
{# SIX #} {# SIX #}
{% if sourceType.type == 'Transfer' and destinationType.type == 'Deposit' %} {#
{% if journalType.type == 'Transfer' and destinationType.type == 'Deposit' %}
<p> <p>
@ -200,10 +476,10 @@
{{ ExpandedForm.text('source_account_revenue', sourceAccount.name) }} {{ ExpandedForm.text('source_account_revenue', sourceAccount.name) }}
{% endif %} {% endif %}
#}
</div> </div>
<div class="box-footer"> <div class="box-footer">
<a href="{{ route('transactions.show', journal.id) }}" class="btn btn-danger">{{ 'cancel'|_ }}</a> <a href="{{ route('transactions.show', group.id) }}" class="btn btn-danger">{{ 'cancel'|_ }}</a>
<button type="submit" id="transaction-btn" class="btn btn-success pull-right"> <button type="submit" id="transaction-btn" class="btn btn-success pull-right">
{{ trans('form.convert_'~sourceType.type) }} {{ trans('form.convert_'~sourceType.type) }}
</button> </button>
@ -216,6 +492,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script type="text/javascript" src="v1/js/lib/typeahead/typeahead.bundle.min.js?v={{ FF_VERSION }}"></script> <script type="text/javascript" src="v1/js/lib/typeahead/typeahead.bundle.min.js?v={{ FF_VERSION }}"></script>
<script type="text/javascript" src="v1/js/ff/common/autocomplete.js?v={{ FF_VERSION }}"></script>
<script type="text/javascript" src="v1/js/ff/transactions/convert.js?v={{ FF_VERSION }}"></script> <script type="text/javascript" src="v1/js/ff/transactions/convert.js?v={{ FF_VERSION }}"></script>
{% endblock %} {% endblock %}

View File

@ -70,20 +70,23 @@
<div class="box-footer"> <div class="box-footer">
<div class="btn-group btn-group-xs"> <div class="btn-group btn-group-xs">
<a href="{{ route('transactions.edit', [transactionGroup.id]) }}" class="btn btn-default"><i class="fa fa-pencil"></i> {{ 'edit'|_ }}</a> <a href="{{ route('transactions.edit', [transactionGroup.id]) }}" class="btn btn-default"><i class="fa fa-pencil"></i> {{ 'edit'|_ }}</a>
{% if type != 'Opening balance' and type != 'Reconciliation' %}
{#<a href="{{ route('transactions.clone', [transactionGroup.id]) }}" class="btn btn-default"><i class="fa fa-copy"></i> {{ 'clone'|_ }}</a>#}
{# {% if groupArray.transactions[0].type != 'withdrawal' %}
{% if type != 'Withdrawal' %} <a href="{{ route('transactions.convert.index', ['withdrawal', transactionGroup.id]) }}" class="btn btn-default"><i class="fa fa-exchange"></i> {{ 'convert_to_withdrawal'|_ }}</a>
<a href="{{ route('transactions.convert', [transactionGroup.id, 'withdrawal']) }}" class="btn btn-default"><i class="fa fa-exchange"></i> {{ 'convert_to_withdrawal'|_ }}</a>
{% endif %} {% endif %}
{% if type != 'Deposit' %}
<a href="{{ route('transactions.convert', [transactionGroup.id, 'deposit']) }}" class="btn btn-default"><i class="fa fa-exchange"></i> {{ 'convert_to_deposit'|_ }}</a> {% if groupArray.transactions[0].type != 'deposit' %}
<a href="{{ route('transactions.convert.index', ['deposit', transactionGroup.id]) }}" class="btn btn-default"><i class="fa fa-exchange"></i> {{ 'convert_to_deposit'|_ }}</a>
{% endif %} {% endif %}
{% if type != 'Transfer' %}
<a href="{{ route('transactions.convert', [transactionGroup.id, 'transfer']) }}" class="btn btn-default"><i class="fa fa-exchange"></i> {{ 'convert_to_transfer'|_ }}</a> {% if groupArray.transactions[0].type != 'transfer' %}
<a href="{{ route('transactions.convert.index', ['transfer', transactionGroup.id]) }}" class="btn btn-default"><i class="fa fa-exchange"></i> {{ 'convert_to_transfer'|_ }}</a>
{% endif %} {% endif %}
#}
{% if groupArray.transactions[0].type != 'opening balance' and groupArray.transactions[0].type != 'reconciliation' %}
CLONE
{#<a href="{{ route('transactions.clone', [transactionGroup.id]) }}" class="btn btn-default"><i class="fa fa-copy"></i> {{ 'clone'|_ }}</a>#}
{% endif %} {% endif %}
{# {#
<a href="{{ route('transactions.delete', [transactionGroup.id]) }}" class="btn btn-danger"><i class="fa fa-trash"></i> {{ 'delete'|_ }}</a> <a href="{{ route('transactions.delete', [transactionGroup.id]) }}" class="btn btn-danger"><i class="fa fa-trash"></i> {{ 'delete'|_ }}</a>

View File

@ -1084,11 +1084,11 @@ try {
Breadcrumbs::register( Breadcrumbs::register(
'transactions.convert.index', 'transactions.convert.index',
function (BreadcrumbsGenerator $breadcrumbs, TransactionType $destinationType, TransactionJournal $journal) { function (BreadcrumbsGenerator $breadcrumbs, TransactionGroup $group, string $groupTitle) {
$breadcrumbs->parent('transactions.show', $journal); $breadcrumbs->parent('transactions.show', $group);
$breadcrumbs->push( $breadcrumbs->push(
trans('firefly.convert_to_' . $destinationType->type, ['description' => limitStringLength($journal->description)]), trans('firefly.breadcrumb_convert_group', ['description' => limitStringLength($groupTitle)]),
route('transactions.convert.index', [strtolower($destinationType->type), $journal->id]) route('transactions.convert.index', [$group->id, 'something'])
); );
} }
); );

View File

@ -532,6 +532,8 @@ Route::group(
// for auto complete // for auto complete
Route::get('accounts', ['uses' => 'Json\AutoCompleteController@accounts', 'as' => 'autocomplete.accounts']); Route::get('accounts', ['uses' => 'Json\AutoCompleteController@accounts', 'as' => 'autocomplete.accounts']);
Route::get('revenue-accounts', ['uses' => 'Json\AutoCompleteController@revenueAccounts', 'as' => 'autocomplete.revenue-accounts']);
Route::get('expense-accounts', ['uses' => 'Json\AutoCompleteController@expenseAccounts', 'as' => 'autocomplete.expense-accounts']);
Route::get('budgets', ['uses' => 'Json\AutoCompleteController@budgets', 'as' => 'autocomplete.budgets']); Route::get('budgets', ['uses' => 'Json\AutoCompleteController@budgets', 'as' => 'autocomplete.budgets']);
Route::get('categories', ['uses' => 'Json\AutoCompleteController@categories', 'as' => 'autocomplete.categories']); Route::get('categories', ['uses' => 'Json\AutoCompleteController@categories', 'as' => 'autocomplete.categories']);
Route::get('currencies', ['uses' => 'Json\AutoCompleteController@currencies', 'as' => 'autocomplete.currencies']); Route::get('currencies', ['uses' => 'Json\AutoCompleteController@currencies', 'as' => 'autocomplete.currencies']);
@ -954,15 +956,13 @@ Route::group(
/** /**
* Transaction Convert Controller * Transaction Convert Controller
*/ */
//Route::group( Route::group(
// ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions/convert', ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions/convert',
// 'as' => 'transactions.convert.'], function () { 'as' => 'transactions.convert.'], static function () {
// // TODO improve these routes Route::get('{transactionType}/{transactionGroup}', ['uses' => 'ConvertController@index', 'as' => 'index']);
// Route::get('{transactionType}/{tj}', ['uses' => 'ConvertController@index', 'as' => 'index']); Route::post('{transactionType}/{transactionGroup}', ['uses' => 'ConvertController@postIndex', 'as' => 'index.post']);
// Route::post('{transactionType}/{tj}', ['uses' => 'ConvertController@postIndex', 'as' => 'index.post']); }
// // TODO end of todo );
//}
//);
/** /**
* Transaction Link Controller * Transaction Link Controller

View File

@ -22,13 +22,16 @@ declare(strict_types=1);
namespace Tests\Feature\Controllers\Transaction; namespace Tests\Feature\Controllers\Transaction;
use Amount;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Log; use Log;
use Mockery; use Mockery;
use Preferences;
use Tests\TestCase; use Tests\TestCase;
/** /**
@ -54,101 +57,35 @@ class BulkControllerTest extends TestCase
* @covers \FireflyIII\Http\Controllers\Transaction\BulkController * @covers \FireflyIII\Http\Controllers\Transaction\BulkController
* @covers \FireflyIII\Http\Controllers\Transaction\BulkController * @covers \FireflyIII\Http\Controllers\Transaction\BulkController
*/ */
public function testEdit(): void public function testEditWithdrawal(): void
{ {
$this->markTestIncomplete('Needs to be rewritten for v4.8.0');
return;
// mock stuff: // mock stuff:
$journalRepos = $this->mock(JournalRepositoryInterface::class); $journalRepos = $this->mockDefaultSession();
$budgetRepos = $this->mock(BudgetRepositoryInterface::class); $budgetRepos = $this->mock(BudgetRepositoryInterface::class);
$userRepos = $this->mock(UserRepositoryInterface::class); $userRepos = $this->mock(UserRepositoryInterface::class);
$collector = $this->mock(GroupCollectorInterface::class);
$withdrawal = $this->getRandomWithdrawal();
$withdrawalArray = $this->getRandomWithdrawalAsArray();
Amount::shouldReceive('formatAnything')->atLeast()->once()->andReturn('-100');
$collector->shouldReceive('setTypes')
->withArgs([[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER]])->atLeast()->once()->andReturnSelf();
$collector->shouldReceive('withCategoryInformation')->atLeast()->once()->andReturnSelf();
$collector->shouldReceive('withBudgetInformation')->atLeast()->once()->andReturnSelf();
$collector->shouldReceive('withTagInformation')->atLeast()->once()->andReturnSelf();
$collector->shouldReceive('setJournalIds')->atLeast()->once()->withArgs([[$withdrawal->id]])->andReturnSelf();
$collector->shouldReceive('getExtractedJournals')->atLeast()->once()->andReturn([$withdrawalArray]);
$userRepos->shouldReceive('hasRole')->withArgs([Mockery::any(), 'owner'])->atLeast()->once()->andReturn(true); $userRepos->shouldReceive('hasRole')->withArgs([Mockery::any(), 'owner'])->atLeast()->once()->andReturn(true);
$budgetRepos->shouldReceive('getActiveBudgets')->andReturn(new Collection); $budgetRepos->shouldReceive('getActiveBudgets')->andReturn(new Collection);
$journalRepos->shouldReceive('getJournalSourceAccounts')->andReturn(new Collection); $journalRepos->shouldReceive('getJournalSourceAccounts')->andReturn(new Collection);
$journalRepos->shouldReceive('getJournalDestinationAccounts')->andReturn(new Collection); $journalRepos->shouldReceive('getJournalDestinationAccounts')->andReturn(new Collection);
$journalRepos->shouldReceive('firstNull')->andReturn(new TransactionJournal);
$journalRepos->shouldReceive('getTransactionType')->andReturn('Transfer'); $journalRepos->shouldReceive('getTransactionType')->andReturn('Transfer');
$journalRepos->shouldReceive('isJournalReconciled')->andReturn(false); $journalRepos->shouldReceive('isJournalReconciled')->andReturn(false);
$transfers = TransactionJournal::where('transaction_type_id', 3)->where('user_id', $this->user()->id)->take(4)->get()->pluck('id')->toArray();
$this->be($this->user()); $this->be($this->user());
$response = $this->get(route('transactions.bulk.edit', $transfers)); $response = $this->get(route('transactions.bulk.edit', [$withdrawal->id]));
$response->assertStatus(200);
$response->assertSee('Bulk edit a number of transactions');
// has bread crumb
$response->assertSee('<ol class="breadcrumb">');
}
/**
* @covers \FireflyIII\Http\Controllers\Transaction\BulkController
*/
public function testEditMultiple(): void
{
$this->markTestIncomplete('Needs to be rewritten for v4.8.0');
return;
// mock stuff:
$journalRepos = $this->mock(JournalRepositoryInterface::class);
$budgetRepos = $this->mock(BudgetRepositoryInterface::class);
$userRepos = $this->mock(UserRepositoryInterface::class);
$userRepos->shouldReceive('hasRole')->withArgs([Mockery::any(), 'owner'])->atLeast()->once()->andReturn(true);
$budgetRepos->shouldReceive('getActiveBudgets')->andReturn(new Collection);
$journalRepos->shouldReceive('firstNull')->andReturn(new TransactionJournal);
$journalRepos->shouldReceive('getJournalSourceAccounts')
->andReturn(new Collection([1, 2, 3]), new Collection, new Collection, new Collection, new Collection([1]));
$journalRepos->shouldReceive('getJournalDestinationAccounts')
->andReturn(new Collection, new Collection([1, 2, 3]), new Collection, new Collection, new Collection([1]));
$journalRepos->shouldReceive('getTransactionType')
->andReturn('Withdrawal', 'Opening balance', 'Withdrawal', 'Withdrawal', 'Withdrawal');
$journalRepos->shouldReceive('isJournalReconciled')
->andReturn(true, false, false, false, false);
// default transactions
$collection = $this->user()->transactionJournals()->take(5)->get();
$allIds = $collection->pluck('id')->toArray();
$route = route('transactions.bulk.edit', implode(',', $allIds));
$this->be($this->user());
$response = $this->get($route);
$response->assertStatus(200);
$response->assertSee('Bulk edit a number of transactions');
$response->assertSessionHas('info');
// has bread crumb
$response->assertSee('<ol class="breadcrumb">');
$response->assertSee('marked as reconciled');
$response->assertSee('multiple source accounts');
$response->assertSee('multiple destination accounts');
}
/**
* @covers \FireflyIII\Http\Controllers\Transaction\BulkController
* @covers \FireflyIII\Http\Controllers\Transaction\BulkController
*/
public function testEditNull(): void
{
$this->markTestIncomplete('Needs to be rewritten for v4.8.0');
return;
// mock stuff:
$journalRepos = $this->mock(JournalRepositoryInterface::class);
$budgetRepos = $this->mock(BudgetRepositoryInterface::class);
$userRepos = $this->mock(UserRepositoryInterface::class);
$userRepos->shouldReceive('hasRole')->withArgs([Mockery::any(), 'owner'])->atLeast()->once()->andReturn(true);
$budgetRepos->shouldReceive('getActiveBudgets')->andReturn(new Collection);
$journalRepos->shouldReceive('getJournalSourceAccounts')->andReturn(new Collection);
$journalRepos->shouldReceive('getJournalDestinationAccounts')->andReturn(new Collection);
$journalRepos->shouldReceive('firstNull')->andReturn(new TransactionJournal, null);
$journalRepos->shouldReceive('getTransactionType')->andReturn('Transfer');
$journalRepos->shouldReceive('isJournalReconciled')->andReturn(false);
$transfers = TransactionJournal::where('transaction_type_id', 3)->where('user_id', $this->user()->id)->take(4)->get()->pluck('id')->toArray();
$this->be($this->user());
$response = $this->get(route('transactions.bulk.edit', $transfers));
$response->assertStatus(200); $response->assertStatus(200);
$response->assertSee('Bulk edit a number of transactions'); $response->assertSee('Bulk edit a number of transactions');
// has bread crumb // has bread crumb
@ -161,34 +98,26 @@ class BulkControllerTest extends TestCase
*/ */
public function testUpdate(): void public function testUpdate(): void
{ {
$this->markTestIncomplete('Needs to be rewritten for v4.8.0');
return;
$tags = ['a', 'b', 'c']; $tags = ['a', 'b', 'c'];
$collection = TransactionJournal::where('transaction_type_id', 1)->where('user_id', $this->user()->id)->take(4)->get(); $budget = $this->getRandomBudget();
$allIds = $collection->pluck('id')->toArray(); $category = $this->getRandomCategory();
$withdrawal = $this->getRandomWithdrawalAsArray();
$data = [ $data = [
'category' => 'Some new category', 'category' => $category->name,
'budget_id' => 1, 'budget_id' => $budget->id,
'tags' => 'a,b,c', 'tags' => 'a,b,c',
'journals' => $allIds, 'journals' => [$withdrawal['transaction_journal_id']],
]; ];
$repository = $this->mock(JournalRepositoryInterface::class); $repository = $this->mockDefaultSession();
$userRepos = $this->mock(UserRepositoryInterface::class);
$repository->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); Preferences::shouldReceive('mark')->atLeast()->once();
$repository->shouldReceive('findNull')->times(4)->andReturn(new TransactionJournal);
$repository->shouldReceive('updateCategory')->times(4)->andReturn(new TransactionJournal()) $repository->shouldReceive('updateBudget')->atLeast()->once()->andReturn(new TransactionJournal())->withArgs([Mockery::any(), $data['budget_id']]);
->withArgs([Mockery::any(), $data['category']]); $repository->shouldReceive('updateCategory')->atLeast()->once()->andReturn(new TransactionJournal())->withArgs([Mockery::any(), $data['category']]);
$repository->shouldReceive('updateTags')->atLeast()->once()->andReturn(new TransactionJournal())->withArgs([Mockery::any(), $tags]);
$repository->shouldReceive('updateBudget')->times(4)->andReturn(new TransactionJournal()) $repository->shouldReceive('findNull')->atLeast()->once()->andReturn(new TransactionJournal);
->withArgs([Mockery::any(), $data['budget_id']]);
$repository->shouldReceive('updateTags')->times(4)->andReturn(new TransactionJournal())
->withArgs([Mockery::any(), ['tags' => $tags]]);
$route = route('transactions.bulk.update'); $route = route('transactions.bulk.update');
@ -198,40 +127,34 @@ class BulkControllerTest extends TestCase
$response->assertSessionHas('success'); $response->assertSessionHas('success');
} }
/** /**
* @covers \FireflyIII\Http\Controllers\Transaction\BulkController * @covers \FireflyIII\Http\Controllers\Transaction\BulkController
* @covers \FireflyIII\Http\Requests\BulkEditJournalRequest * @covers \FireflyIII\Http\Requests\BulkEditJournalRequest
*/ */
public function testUpdateNull(): void public function testUpdateIgnoreAll(): void
{ {
$this->markTestIncomplete('Needs to be rewritten for v4.8.0'); $budget = $this->getRandomBudget();
$category = $this->getRandomCategory();
return; $withdrawal = $this->getRandomWithdrawalAsArray();
$tags = ['a', 'b', 'c'];
$collection = TransactionJournal::where('transaction_type_id', 1)->where('user_id', $this->user()->id)->take(4)->get();
$allIds = $collection->pluck('id')->toArray();
$data = [ $data = [
'category' => 'Some new category', 'category' => $category->name,
'budget_id' => 1, 'budget_id' => $budget->id,
'tags' => 'a,b,c', 'tags' => 'a,b,c',
'journals' => $allIds, 'journals' => [$withdrawal['transaction_journal_id']],
'ignore_category' => '1',
'ignore_budget' => '1',
'ignore_tags' => '1',
]; ];
$repository = $this->mock(JournalRepositoryInterface::class); $repository = $this->mockDefaultSession();
$userRepos = $this->mock(UserRepositoryInterface::class);
$repository->shouldReceive('firstNull')->once()->andReturn(new TransactionJournal); Preferences::shouldReceive('mark')->atLeast()->once();
$repository->shouldReceive('findNull')->times(4)->andReturn(new TransactionJournal, null);
$repository->shouldReceive('updateCategory')->times(1)->andReturn(new TransactionJournal()) $repository->shouldNotReceive('updateBudget');
->withArgs([Mockery::any(), $data['category']]); $repository->shouldNotReceive('updateCategory');
$repository->shouldNotReceive('updateTags');
$repository->shouldReceive('updateBudget')->times(1)->andReturn(new TransactionJournal()) $repository->shouldReceive('findNull')->atLeast()->once()->andReturn(new TransactionJournal);
->withArgs([Mockery::any(), $data['budget_id']]);
$repository->shouldReceive('updateTags')->times(1)->andReturn(new TransactionJournal())
->withArgs([Mockery::any(), ['tags' => $tags]]);
$route = route('transactions.bulk.update'); $route = route('transactions.bulk.update');