diff --git a/app/Factory/BillFactory.php b/app/Factory/BillFactory.php index 79647a75e3..fadcf629b0 100644 --- a/app/Factory/BillFactory.php +++ b/app/Factory/BillFactory.php @@ -27,6 +27,7 @@ namespace FireflyIII\Factory; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Bill; use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; use FireflyIII\Services\Internal\Support\BillServiceTrait; use FireflyIII\User; use Illuminate\Database\QueryException; @@ -37,7 +38,7 @@ use Log; */ class BillFactory { - use BillServiceTrait; + use BillServiceTrait, CreatesObjectGroups; /** @var User */ private $user; @@ -97,6 +98,24 @@ class BillFactory $this->updateNote($bill, $data['notes']); } + $objectGroupTitle = $data['object_group'] ?? ''; + if ('' !== $objectGroupTitle) { + $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); + if (null !== $objectGroup) { + $bill->objectGroups()->sync([$objectGroup->id]); + $bill->save(); + } + } + // try also with ID: + $objectGroupId = (int) ($data['object_group_id'] ?? 0); + if (0 !== $objectGroupId) { + $objectGroup = $this->findObjectGroupById($objectGroupId); + if (null !== $objectGroup) { + $bill->objectGroups()->sync([$objectGroup->id]); + $bill->save(); + } + } + return $bill; } diff --git a/app/Http/Controllers/Bill/CreateController.php b/app/Http/Controllers/Bill/CreateController.php new file mode 100644 index 0000000000..5f94d39e1f --- /dev/null +++ b/app/Http/Controllers/Bill/CreateController.php @@ -0,0 +1,130 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Bill; + + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Http\Requests\BillStoreRequest; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; + +/** + * Class CreateController + */ +class CreateController extends Controller +{ + private AttachmentHelperInterface $attachments; + private BillRepositoryInterface $repository; + + /** + * BillController constructor. + * + * @codeCoverageIgnore + */ + public function __construct() + { + parent::__construct(); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', (string) trans('firefly.bills')); + app('view')->share('mainTitleIcon', 'fa-calendar-o'); + $this->attachments = app(AttachmentHelperInterface::class); + $this->repository = app(BillRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * Create a new bill. + * + * @param Request $request + * + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function create(Request $request) + { + $periods = []; + /** @var array $billPeriods */ + $billPeriods = config('firefly.bill_periods'); + foreach ($billPeriods as $current) { + $periods[$current] = strtolower((string) trans('firefly.repeat_freq_' . $current)); + } + $subTitle = (string) trans('firefly.create_new_bill'); + $defaultCurrency = app('amount')->getDefaultCurrency(); + + // put previous url in session if not redirect from store (not "create another"). + if (true !== session('bills.create.fromStore')) { + $this->rememberPreviousUri('bills.create.uri'); + } + $request->session()->forget('bills.create.fromStore'); + + return view('bills.create', compact('periods', 'subTitle', 'defaultCurrency')); + } + + /** + * Store a new bill. + * + * @param BillStoreRequest $request + * + * @return RedirectResponse + * + */ + public function store(BillStoreRequest $request): RedirectResponse + { + $billData = $request->getBillData(); + $billData['active'] = true; + try { + $bill = $this->repository->store($billData); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + $request->session()->flash('error', (string) trans('firefly.bill_store_error')); + + return redirect(route('bills.create'))->withInput(); + } + $request->session()->flash('success', (string) trans('firefly.stored_new_bill', ['name' => $bill->name])); + app('preferences')->mark(); + + /** @var array $files */ + $files = $request->hasFile('attachments') ? $request->file('attachments') : null; + if (null !== $files && !auth()->user()->hasRole('demo')) { + $this->attachments->saveAttachmentsForModel($bill, $files); + } + if (null !== $files && auth()->user()->hasRole('demo')) { + session()->flash('info', (string) trans('firefly.no_att_demo_user')); + } + + if (count($this->attachments->getMessages()->get('attachments')) > 0) { + $request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); // @codeCoverageIgnore + } + + return redirect(route('rules.create-from-bill', [$bill->id])); + } +} diff --git a/app/Http/Controllers/Bill/DeleteController.php b/app/Http/Controllers/Bill/DeleteController.php new file mode 100644 index 0000000000..8c25043d9d --- /dev/null +++ b/app/Http/Controllers/Bill/DeleteController.php @@ -0,0 +1,99 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Bill; + + +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Bill; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use Illuminate\Contracts\View\Factory; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Routing\Redirector; +use Illuminate\View\View; + +/** + * Class DeleteController + */ +class DeleteController extends Controller +{ + private BillRepositoryInterface $repository; + + /** + * BillController constructor. + * + * @codeCoverageIgnore + */ + public function __construct() + { + parent::__construct(); + + app('view')->share('showBudget', true); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', (string) trans('firefly.bills')); + app('view')->share('mainTitleIcon', 'fa-calendar-o'); + $this->repository = app(BillRepositoryInterface::class); + + return $next($request); + } + ); + } + /** + * Delete a bill. + * + * @param Bill $bill + * + * @return Factory|View + */ + public function delete(Bill $bill) + { + // put previous url in session + $this->rememberPreviousUri('bills.delete.uri'); + $subTitle = (string) trans('firefly.delete_bill', ['name' => $bill->name]); + + return view('bills.delete', compact('bill', 'subTitle')); + } + + /** + * Destroy a bill. + * + * @param Request $request + * @param Bill $bill + * + * @return RedirectResponse|Redirector + */ + public function destroy(Request $request, Bill $bill) + { + $name = $bill->name; + $this->repository->destroy($bill); + + $request->session()->flash('success', (string) trans('firefly.deleted_bill', ['name' => $name])); + app('preferences')->mark(); + + return redirect($this->getPreviousUri('bills.delete.uri')); + } +} diff --git a/app/Http/Controllers/Bill/EditController.php b/app/Http/Controllers/Bill/EditController.php new file mode 100644 index 0000000000..c817f40caf --- /dev/null +++ b/app/Http/Controllers/Bill/EditController.php @@ -0,0 +1,155 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Bill; + + +use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Http\Requests\BillUpdateRequest; +use FireflyIII\Models\Bill; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; + +/** + * Class EditController + */ +class EditController extends Controller +{ + private AttachmentHelperInterface $attachments; + private BillRepositoryInterface $repository; + + /** + * BillController constructor. + * + * @codeCoverageIgnore + */ + public function __construct() + { + parent::__construct(); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', (string) trans('firefly.bills')); + app('view')->share('mainTitleIcon', 'fa-calendar-o'); + $this->attachments = app(AttachmentHelperInterface::class); + $this->repository = app(BillRepositoryInterface::class); + + return $next($request); + } + ); + } + + + /** + * Edit a bill. + * + * @param Request $request + * @param Bill $bill + * + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function edit(Request $request, Bill $bill) + { + $periods = []; + /** @var array $billPeriods */ + $billPeriods = config('firefly.bill_periods'); + + foreach ($billPeriods as $current) { + $periods[$current] = (string) trans('firefly.' . $current); + } + + $subTitle = (string) trans('firefly.edit_bill', ['name' => $bill->name]); + + // put previous url in session if not redirect from store (not "return_to_edit"). + if (true !== session('bills.edit.fromUpdate')) { + $this->rememberPreviousUri('bills.edit.uri'); + } + + $currency = app('amount')->getDefaultCurrency(); + $bill->amount_min = round((float) $bill->amount_min, $currency->decimal_places); + $bill->amount_max = round((float) $bill->amount_max, $currency->decimal_places); + $rules = $this->repository->getRulesForBill($bill); + $defaultCurrency = app('amount')->getDefaultCurrency(); + + // code to handle active-checkboxes + $hasOldInput = null !== $request->old('_token'); + + $preFilled = [ + 'notes' => $this->repository->getNoteText($bill), + 'transaction_currency_id' => $bill->transaction_currency_id, + 'active' => $hasOldInput ? (bool) $request->old('active') : $bill->active, + 'object_group' => $bill->objectGroups->first() ? $bill->objectGroups->first()->title : '', + ]; + + $request->session()->flash('preFilled', $preFilled); + $request->session()->forget('bills.edit.fromUpdate'); + + return view('bills.edit', compact('subTitle', 'periods', 'rules', 'bill', 'defaultCurrency', 'preFilled')); + } + + + /** + * Update a bill. + * + * @param BillUpdateRequest $request + * @param Bill $bill + * + * @return RedirectResponse + */ + public function update(BillUpdateRequest $request, Bill $bill): RedirectResponse + { + $billData = $request->getBillData(); + $bill = $this->repository->update($bill, $billData); + + $request->session()->flash('success', (string) trans('firefly.updated_bill', ['name' => $bill->name])); + app('preferences')->mark(); + + /** @var array $files */ + $files = $request->hasFile('attachments') ? $request->file('attachments') : null; + if (null !== $files && !auth()->user()->hasRole('demo')) { + $this->attachments->saveAttachmentsForModel($bill, $files); + } + if (null !== $files && auth()->user()->hasRole('demo')) { + session()->flash('info',(string)trans('firefly.no_att_demo_user')); + } + + // flash messages + if (count($this->attachments->getMessages()->get('attachments')) > 0) { + $request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); // @codeCoverageIgnore + } + $redirect = redirect($this->getPreviousUri('bills.edit.uri')); + + if (1 === (int) $request->get('return_to_edit')) { + // @codeCoverageIgnoreStart + $request->session()->put('bills.edit.fromUpdate', true); + + $redirect = redirect(route('bills.edit', [$bill->id]))->withInput(['return_to_edit' => 1]); + // @codeCoverageIgnoreEnd + } + + return $redirect; + } +} diff --git a/app/Http/Controllers/Bill/IndexController.php b/app/Http/Controllers/Bill/IndexController.php new file mode 100644 index 0000000000..8e46fd82cd --- /dev/null +++ b/app/Http/Controllers/Bill/IndexController.php @@ -0,0 +1,171 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Bill; + + +use Carbon\Carbon; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Bill; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Transformers\BillTransformer; +use Symfony\Component\HttpFoundation\ParameterBag; + +/** + * Class IndexController + */ +class IndexController extends Controller +{ + private BillRepositoryInterface $repository; + + /** + * BillController constructor. + * + * @codeCoverageIgnore + */ + public function __construct() + { + parent::__construct(); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', (string) trans('firefly.bills')); + app('view')->share('mainTitleIcon', 'fa-calendar-o'); + $this->repository = app(BillRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * Show all bills. + */ + public function index() + { + $start = session('start'); + $end = session('end'); + $collection = $this->repository->getBills(); + $total = $collection->count(); + + $defaultCurrency = app('amount')->getDefaultCurrency(); + $parameters = new ParameterBag; + $parameters->set('start', $start); + $parameters->set('end', $end); + + /** @var BillTransformer $transformer */ + $transformer = app(BillTransformer::class); + $transformer->setParameters($parameters); + + // loop all bills, convert to array and add rules and stuff. + $rules = $this->repository->getRulesForBills($collection); + + // make bill groups: + $bills = [ + 0 => [ // the index is the order, not the ID. + 'object_group_id' => 0, + 'object_group_title' => (string) trans('firefly.default_group_title_name'), + 'bills' => [], + ], + ]; + + + /** @var Bill $bill */ + foreach ($collection as $bill) { + $array = $transformer->transform($bill); + $groupOrder = (int) $array['object_group_order']; + // make group array if necessary: + $bills[$groupOrder] = $bills[$groupOrder] ?? [ + 'object_group_id' => $array['object_group_id'], + 'object_group_title' => $array['object_group_title'], + 'bills' => [], + ]; + + $nextExpectedMatch = new Carbon($array['next_expected_match']); + $array['next_expected_match_diff'] = $nextExpectedMatch->isToday() + ? trans('firefly.today') + : $nextExpectedMatch->diffForHumans( + today(), Carbon::DIFF_RELATIVE_TO_NOW + ); + $currency = $bill->transactionCurrency ?? $defaultCurrency; + $array['currency_id'] = $currency->id; + $array['currency_name'] = $currency->name; + $array['currency_symbol'] = $currency->symbol; + $array['currency_code'] = $currency->code; + $array['currency_decimal_places'] = $currency->decimal_places; + $array['attachments'] = $this->repository->getAttachments($bill); + $array['rules'] = $rules[$bill['id']] ?? []; + $bills[$groupOrder]['bills'][] = $array; + } + + // order by key + ksort($bills); + + // summarise per currency / per group. + $sums = $this->getSums($bills); + + return view('bills.index', compact('bills', 'sums', 'total')); + } + + + /** + * @param array $bills + * + * @return array + */ + private function getSums(array $bills): array + { + $sums = []; + + /** @var array $group */ + foreach ($bills as $groupOrder => $group) { + /** @var array $bill */ + foreach ($group['bills'] as $bill) { + if (false === $bill['active']) { + continue; + } + if (0 === count($bill['pay_dates'])) { + continue; + } + /** @var TransactionCurrency $currency */ + $currencyId = $bill['currency_id']; + $sums[$groupOrder][$currencyId] = $sums[$groupOrder][$currencyId] ?? [ + 'currency_id' => $currencyId, + 'currency_code' => $bill['currency_code'], + 'currency_name' => $bill['currency_name'], + 'currency_symbol' => $bill['currency_symbol'], + 'currency_decimal_places' => $bill['currency_decimal_places'], + 'avg' => '0', + ]; + + $avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2'); + $avg = bcmul($avg, (string) count($bill['pay_dates'])); + $sums[$groupOrder][$currencyId]['avg'] = bcadd($sums[$groupOrder][$currencyId]['avg'], $avg); + } + } + + return $sums; + } +} diff --git a/app/Http/Controllers/Bill/ShowController.php b/app/Http/Controllers/Bill/ShowController.php new file mode 100644 index 0000000000..8cc8b0b6dd --- /dev/null +++ b/app/Http/Controllers/Bill/ShowController.php @@ -0,0 +1,195 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Bill; + + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\Attachment; +use FireflyIII\Models\Bill; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\TransactionRules\TransactionMatcher; +use FireflyIII\Transformers\AttachmentTransformer; +use FireflyIII\Transformers\BillTransformer; +use Illuminate\Contracts\View\Factory; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Routing\Redirector; +use Illuminate\Support\Collection; +use Illuminate\View\View; +use League\Fractal\Manager; +use League\Fractal\Resource\Item; +use League\Fractal\Serializer\DataArraySerializer; +use Symfony\Component\HttpFoundation\ParameterBag; + +/** + * Class ShowController + */ +class ShowController extends Controller +{ + private BillRepositoryInterface $repository; + + /** + * BillController constructor. + * + * @codeCoverageIgnore + */ + public function __construct() + { + parent::__construct(); + + app('view')->share('showBudget', true); + + $this->middleware( + function ($request, $next) { + app('view')->share('title', (string) trans('firefly.bills')); + app('view')->share('mainTitleIcon', 'fa-calendar-o'); + $this->repository = app(BillRepositoryInterface::class); + + return $next($request); + } + ); + } + /** + * Rescan bills for transactions. + * + * @param Request $request + * @param Bill $bill + * + * @throws FireflyException + * @return RedirectResponse|Redirector + */ + public function rescan(Request $request, Bill $bill) + { + $total = 0; + if (false === $bill->active) { + $request->session()->flash('warning', (string) trans('firefly.cannot_scan_inactive_bill')); + + return redirect(route('bills.show', [$bill->id])); + } + $set = new Collection; + if (true === $bill->active) { + $set = $this->repository->getRulesForBill($bill); + $total = 0; + } + if (0 === $set->count()) { + $request->session()->flash('error', (string) trans('firefly.no_rules_for_bill')); + + return redirect(route('bills.show', [$bill->id])); + } + + // unlink all journals: + $this->repository->unlinkAll($bill); + + foreach ($set as $rule) { + // simply fire off all rules? + /** @var TransactionMatcher $matcher */ + $matcher = app(TransactionMatcher::class); + $matcher->setSearchLimit(100000); // large upper limit + $matcher->setTriggeredLimit(100000); // large upper limit + $matcher->setRule($rule); + $matchingTransactions = $matcher->findTransactionsByRule(); + $total += count($matchingTransactions); + $this->repository->linkCollectionToBill($bill, $matchingTransactions); + } + + + $request->session()->flash('success', (string) trans_choice('firefly.rescanned_bill', $total)); + app('preferences')->mark(); + + return redirect(route('bills.show', [$bill->id])); + } + + + + /** + * Show a bill. + * + * @param Request $request + * @param Bill $bill + * + * @return Factory|View + */ + public function show(Request $request, Bill $bill) + { + // add info about rules: + $rules = $this->repository->getRulesForBill($bill); + $subTitle = $bill->name; + /** @var Carbon $start */ + $start = session('start'); + /** @var Carbon $end */ + $end = session('end'); + $year = $start->year; + $page = (int) $request->get('page'); + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; + $yearAverage = $this->repository->getYearAverage($bill, $start); + $overallAverage = $this->repository->getOverallAverage($bill); + $manager = new Manager(); + $manager->setSerializer(new DataArraySerializer()); + $manager->parseIncludes(['attachments', 'notes']); + + // Make a resource out of the data and + $parameters = new ParameterBag(); + $parameters->set('start', $start); + $parameters->set('end', $end); + + /** @var BillTransformer $transformer */ + $transformer = app(BillTransformer::class); + $transformer->setParameters($parameters); + + $resource = new Item($bill, $transformer, 'bill'); + $object = $manager->createData($resource)->toArray(); + $object['data']['currency'] = $bill->transactionCurrency; + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setBill($bill)->setLimit($pageSize)->setPage($page)->withBudgetInformation() + ->withCategoryInformation()->withAccountInformation(); + $groups = $collector->getPaginatedGroups(); + $groups->setPath(route('bills.show', [$bill->id])); + + // transform any attachments as well. + $collection = $this->repository->getAttachments($bill); + $attachments = new Collection; + + // @codeCoverageIgnoreStart + if ($collection->count() > 0) { + /** @var AttachmentTransformer $transformer */ + $transformer = app(AttachmentTransformer::class); + $attachments = $collection->each( + static function (Attachment $attachment) use ($transformer) { + return $transformer->transform($attachment); + } + ); + } + + // @codeCoverageIgnoreEnd + + + return view('bills.show', compact('attachments', 'groups', 'rules', 'yearAverage', 'overallAverage', 'year', 'object', 'bill', 'subTitle')); + } + +} diff --git a/app/Http/Controllers/BillController.php b/app/Http/Controllers/BillController.php deleted file mode 100644 index ee674fa8ab..0000000000 --- a/app/Http/Controllers/BillController.php +++ /dev/null @@ -1,483 +0,0 @@ -. - */ -declare(strict_types=1); - -namespace FireflyIII\Http\Controllers; - -use Carbon\Carbon; -use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; -use FireflyIII\Helpers\Collector\GroupCollectorInterface; -use FireflyIII\Http\Requests\BillFormRequest; -use FireflyIII\Models\Attachment; -use FireflyIII\Models\Bill; -use FireflyIII\Models\TransactionCurrency; -use FireflyIII\Repositories\Bill\BillRepositoryInterface; -use FireflyIII\TransactionRules\TransactionMatcher; -use FireflyIII\Transformers\AttachmentTransformer; -use FireflyIII\Transformers\BillTransformer; -use Illuminate\Contracts\View\Factory; -use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; -use Illuminate\Routing\Redirector; -use Illuminate\Support\Collection; -use Illuminate\View\View; -use League\Fractal\Manager; -use League\Fractal\Resource\Item; -use League\Fractal\Serializer\DataArraySerializer; -use Log; -use Symfony\Component\HttpFoundation\ParameterBag; - -/** - * Class BillController. - * - */ -class BillController extends Controller -{ - /** @var AttachmentHelperInterface Helper for attachments. */ - private $attachments; - /** @var BillRepositoryInterface Bill repository */ - private $billRepository; - - /** - * BillController constructor. - * - * @codeCoverageIgnore - */ - public function __construct() - { - parent::__construct(); - - app('view')->share('showBudget', true); - - $this->middleware( - function ($request, $next) { - app('view')->share('title', (string) trans('firefly.bills')); - app('view')->share('mainTitleIcon', 'fa-calendar-o'); - $this->attachments = app(AttachmentHelperInterface::class); - $this->billRepository = app(BillRepositoryInterface::class); - - return $next($request); - } - ); - } - - /** - * Create a new bill. - * - * @param Request $request - * - * @return Factory|View - */ - public function create(Request $request) - { - $periods = []; - /** @var array $billPeriods */ - $billPeriods = config('firefly.bill_periods'); - foreach ($billPeriods as $current) { - $periods[$current] = strtolower((string) trans('firefly.repeat_freq_' . $current)); - } - $subTitle = (string) trans('firefly.create_new_bill'); - $defaultCurrency = app('amount')->getDefaultCurrency(); - - // put previous url in session if not redirect from store (not "create another"). - if (true !== session('bills.create.fromStore')) { - $this->rememberPreviousUri('bills.create.uri'); - } - $request->session()->forget('bills.create.fromStore'); - - return view('bills.create', compact('periods', 'subTitle', 'defaultCurrency')); - } - - /** - * Delete a bill. - * - * @param Bill $bill - * - * @return Factory|View - */ - public function delete(Bill $bill) - { - // put previous url in session - $this->rememberPreviousUri('bills.delete.uri'); - $subTitle = (string) trans('firefly.delete_bill', ['name' => $bill->name]); - - return view('bills.delete', compact('bill', 'subTitle')); - } - - /** - * Destroy a bill. - * - * @param Request $request - * @param Bill $bill - * - * @return RedirectResponse|Redirector - */ - public function destroy(Request $request, Bill $bill) - { - $name = $bill->name; - $this->billRepository->destroy($bill); - - $request->session()->flash('success', (string) trans('firefly.deleted_bill', ['name' => $name])); - app('preferences')->mark(); - - return redirect($this->getPreviousUri('bills.delete.uri')); - } - - /** - * Edit a bill. - * - * @param Request $request - * @param Bill $bill - * - * @return Factory|View - */ - public function edit(Request $request, Bill $bill) - { - $periods = []; - /** @var array $billPeriods */ - $billPeriods = config('firefly.bill_periods'); - - foreach ($billPeriods as $current) { - $periods[$current] = (string) trans('firefly.' . $current); - } - - $subTitle = (string) trans('firefly.edit_bill', ['name' => $bill->name]); - - // put previous url in session if not redirect from store (not "return_to_edit"). - if (true !== session('bills.edit.fromUpdate')) { - $this->rememberPreviousUri('bills.edit.uri'); - } - - $currency = app('amount')->getDefaultCurrency(); - $bill->amount_min = round((float) $bill->amount_min, $currency->decimal_places); - $bill->amount_max = round((float) $bill->amount_max, $currency->decimal_places); - $rules = $this->billRepository->getRulesForBill($bill); - $defaultCurrency = app('amount')->getDefaultCurrency(); - - // code to handle active-checkboxes - $hasOldInput = null !== $request->old('_token'); - - $preFilled = [ - 'notes' => $this->billRepository->getNoteText($bill), - 'transaction_currency_id' => $bill->transaction_currency_id, - 'active' => $hasOldInput ? (bool) $request->old('active') : $bill->active, - ]; - - $request->session()->flash('preFilled', $preFilled); - $request->session()->forget('bills.edit.fromUpdate'); - - return view('bills.edit', compact('subTitle', 'periods', 'rules', 'bill', 'defaultCurrency', 'preFilled')); - } - - /** - * Show all bills. - * - * @return Factory|View - */ - public function index() - { - $start = session('start'); - $end = session('end'); - $unfiltered = $this->billRepository->getBills(); - - $defaultCurrency = app('amount')->getDefaultCurrency(); - $parameters = new ParameterBag(); - $parameters->set('start', $start); - $parameters->set('end', $end); - - /** @var BillTransformer $transformer */ - $transformer = app(BillTransformer::class); - $transformer->setParameters($parameters); - - /** @var Collection $bills */ - $bills = $unfiltered->map( - function (Bill $bill) use ($transformer, $defaultCurrency) { - $return = $transformer->transform($bill); - $nextExpectedMatch = new Carbon($return['next_expected_match']); - $return['next_expected_match_diff'] = $nextExpectedMatch->isToday() - ? trans('firefly.today') - : $nextExpectedMatch->diffForHumans( - today(), Carbon::DIFF_RELATIVE_TO_NOW - ); - $currency = $bill->transactionCurrency ?? $defaultCurrency; - $return['currency_id'] = $currency->id; - $return['currency_name'] = $currency->name; - $return['currency_symbol'] = $currency->symbol; - $return['currency_code'] = $currency->code; - $return['currency_decimal_places'] = $currency->decimal_places; - $return['attachments'] = $this->billRepository->getAttachments($bill); - - return $return; - } - ); - - // add info about rules: - $rules = $this->billRepository->getRulesForBills($unfiltered); - $bills = $bills->map( - static function (array $bill) use ($rules) { - $bill['rules'] = $rules[$bill['id']] ?? []; - - return $bill; - } - ); - - // summarise per currency: - $sums = $this->getSums($bills); - - return view('bills.index', compact('bills', 'sums')); - } - - /** - * Rescan bills for transactions. - * - * @param Request $request - * @param Bill $bill - * - * @throws FireflyException - * @return RedirectResponse|Redirector - */ - public function rescan(Request $request, Bill $bill) - { - $total = 0; - if (false === $bill->active) { - $request->session()->flash('warning', (string) trans('firefly.cannot_scan_inactive_bill')); - - return redirect(route('bills.show', [$bill->id])); - } - $set = new Collection; - if (true === $bill->active) { - $set = $this->billRepository->getRulesForBill($bill); - $total = 0; - } - if (0 === $set->count()) { - $request->session()->flash('error', (string) trans('firefly.no_rules_for_bill')); - - return redirect(route('bills.show', [$bill->id])); - } - - // unlink all journals: - $this->billRepository->unlinkAll($bill); - - foreach ($set as $rule) { - // simply fire off all rules? - /** @var TransactionMatcher $matcher */ - $matcher = app(TransactionMatcher::class); - $matcher->setSearchLimit(100000); // large upper limit - $matcher->setTriggeredLimit(100000); // large upper limit - $matcher->setRule($rule); - $matchingTransactions = $matcher->findTransactionsByRule(); - $total += count($matchingTransactions); - $this->billRepository->linkCollectionToBill($bill, $matchingTransactions); - } - - - $request->session()->flash('success', (string) trans_choice('firefly.rescanned_bill', $total)); - app('preferences')->mark(); - - return redirect(route('bills.show', [$bill->id])); - } - - /** - * Show a bill. - * - * @param Request $request - * @param Bill $bill - * - * @return Factory|View - */ - public function show(Request $request, Bill $bill) - { - // add info about rules: - $rules = $this->billRepository->getRulesForBill($bill); - $subTitle = $bill->name; - /** @var Carbon $start */ - $start = session('start'); - /** @var Carbon $end */ - $end = session('end'); - $year = $start->year; - $page = (int) $request->get('page'); - $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; - $yearAverage = $this->billRepository->getYearAverage($bill, $start); - $overallAverage = $this->billRepository->getOverallAverage($bill); - $manager = new Manager(); - $manager->setSerializer(new DataArraySerializer()); - $manager->parseIncludes(['attachments', 'notes']); - - // Make a resource out of the data and - $parameters = new ParameterBag(); - $parameters->set('start', $start); - $parameters->set('end', $end); - - /** @var BillTransformer $transformer */ - $transformer = app(BillTransformer::class); - $transformer->setParameters($parameters); - - $resource = new Item($bill, $transformer, 'bill'); - $object = $manager->createData($resource)->toArray(); - $object['data']['currency'] = $bill->transactionCurrency; - - /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); - $collector->setBill($bill)->setLimit($pageSize)->setPage($page)->withBudgetInformation() - ->withCategoryInformation()->withAccountInformation(); - $groups = $collector->getPaginatedGroups(); - $groups->setPath(route('bills.show', [$bill->id])); - - // transform any attachments as well. - $collection = $this->billRepository->getAttachments($bill); - $attachments = new Collection; - - // @codeCoverageIgnoreStart - if ($collection->count() > 0) { - /** @var AttachmentTransformer $transformer */ - $transformer = app(AttachmentTransformer::class); - $attachments = $collection->each( - static function (Attachment $attachment) use ($transformer) { - return $transformer->transform($attachment); - } - ); - } - - // @codeCoverageIgnoreEnd - - - return view('bills.show', compact('attachments', 'groups', 'rules', 'yearAverage', 'overallAverage', 'year', 'object', 'bill', 'subTitle')); - } - - - /** - * Store a new bill. - * - * @param BillFormRequest $request - * - * @return RedirectResponse - * - */ - public function store(BillFormRequest $request): RedirectResponse - { - $billData = $request->getBillData(); - $billData['active'] = true; - try { - $bill = $this->billRepository->store($billData); - } catch (FireflyException $e) { - Log::error($e->getMessage()); - $request->session()->flash('error', (string) trans('firefly.bill_store_error')); - - return redirect(route('bills.create'))->withInput(); - } - $request->session()->flash('success', (string) trans('firefly.stored_new_bill', ['name' => $bill->name])); - app('preferences')->mark(); - - /** @var array $files */ - $files = $request->hasFile('attachments') ? $request->file('attachments') : null; - if (null !== $files && !auth()->user()->hasRole('demo')) { - $this->attachments->saveAttachmentsForModel($bill, $files); - } - if (null !== $files && auth()->user()->hasRole('demo')) { - session()->flash('info',(string)trans('firefly.no_att_demo_user')); - } - - if (count($this->attachments->getMessages()->get('attachments')) > 0) { - $request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); // @codeCoverageIgnore - } - - return redirect(route('rules.create-from-bill', [$bill->id])); - } - - /** - * Update a bill. - * - * @param BillFormRequest $request - * @param Bill $bill - * - * @return RedirectResponse - */ - public function update(BillFormRequest $request, Bill $bill): RedirectResponse - { - $billData = $request->getBillData(); - $bill = $this->billRepository->update($bill, $billData); - - $request->session()->flash('success', (string) trans('firefly.updated_bill', ['name' => $bill->name])); - app('preferences')->mark(); - - /** @var array $files */ - $files = $request->hasFile('attachments') ? $request->file('attachments') : null; - if (null !== $files && !auth()->user()->hasRole('demo')) { - $this->attachments->saveAttachmentsForModel($bill, $files); - } - if (null !== $files && auth()->user()->hasRole('demo')) { - session()->flash('info',(string)trans('firefly.no_att_demo_user')); - } - - // flash messages - if (count($this->attachments->getMessages()->get('attachments')) > 0) { - $request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); // @codeCoverageIgnore - } - $redirect = redirect($this->getPreviousUri('bills.edit.uri')); - - if (1 === (int) $request->get('return_to_edit')) { - // @codeCoverageIgnoreStart - $request->session()->put('bills.edit.fromUpdate', true); - - $redirect = redirect(route('bills.edit', [$bill->id]))->withInput(['return_to_edit' => 1]); - // @codeCoverageIgnoreEnd - } - - return $redirect; - } - - /** - * @param Collection $bills - * - * @return array - */ - private function getSums(Collection $bills): array - { - $sums = []; - - /** @var array $bill */ - foreach ($bills as $bill) { - if (false === $bill['active']) { - continue; - } - if (0 === count($bill['pay_dates'])) { - continue; - } - /** @var TransactionCurrency $currency */ - $currencyId = $bill['currency_id']; - $sums[$currencyId] = $sums[$currencyId] ?? [ - 'currency_id' => $currencyId, - 'currency_code' => $bill['currency_code'], - 'currency_name' => $bill['currency_name'], - 'currency_symbol' => $bill['currency_symbol'], - 'currency_decimal_places' => $bill['currency_decimal_places'], - 'avg' => '0', - ]; - - $avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2'); - $avg = bcmul($avg, (string) count($bill['pay_dates'])); - $sums[$currencyId]['avg'] = bcadd($sums[$currencyId]['avg'], $avg); - } - - return $sums; - } -} diff --git a/app/Http/Requests/BillStoreRequest.php b/app/Http/Requests/BillStoreRequest.php new file mode 100644 index 0000000000..2ddfefd373 --- /dev/null +++ b/app/Http/Requests/BillStoreRequest.php @@ -0,0 +1,81 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Http\Requests; + +/** + * Class BillStoreRequest. + */ +class BillStoreRequest extends Request +{ + /** + * Verify the request. + * + * @return bool + */ + public function authorize(): bool + { + // Only allow logged in users + return auth()->check(); + } + + /** + * Returns the data required by the controller. + * + * @return array + */ + public function getBillData(): array + { + return [ + 'name' => $this->string('name'), + 'amount_min' => $this->string('amount_min'), + 'currency_id' => $this->integer('transaction_currency_id'), + 'currency_code' => '', + 'amount_max' => $this->string('amount_max'), + 'date' => $this->date('date'), + 'repeat_freq' => $this->string('repeat_freq'), + 'skip' => $this->integer('skip'), + 'notes' => $this->nlString('notes'), + 'active' => $this->boolean('active'), + 'object_group' => $this->string('object_group'), + ]; + } + + /** + * Rules for this request. + * + * @return array + */ + public function rules(): array + { + return [ + 'name' => 'required|between:1,255|uniqueObjectForUser:bills,name', + 'amount_min' => 'required|numeric|more:0|max:1000000000', + 'amount_max' => 'required|numeric|more:0|max:1000000000', + 'transaction_currency_id' => 'required|exists:transaction_currencies,id', + 'date' => 'required|date', + 'repeat_freq' => 'required|in:weekly,monthly,quarterly,half-year,yearly', + 'skip' => 'required|between:0,31', + 'active' => 'boolean', + ]; + } +} diff --git a/app/Http/Requests/BillFormRequest.php b/app/Http/Requests/BillUpdateRequest.php similarity index 86% rename from app/Http/Requests/BillFormRequest.php rename to app/Http/Requests/BillUpdateRequest.php index 2ca1a242cc..49a5ed6c9a 100644 --- a/app/Http/Requests/BillFormRequest.php +++ b/app/Http/Requests/BillUpdateRequest.php @@ -1,6 +1,6 @@ $this->integer('skip'), 'notes' => $this->nlString('notes'), 'active' => $this->boolean('active'), + 'object_group' => $this->string('object_group'), ]; } @@ -68,15 +69,11 @@ class BillFormRequest extends Request */ public function rules(): array { - $nameRule = 'required|between:1,255|uniqueObjectForUser:bills,name'; /** @var Bill $bill */ $bill = $this->route()->parameter('bill'); - if (null !== $bill) { - $nameRule = 'required|between:1,255|uniqueObjectForUser:bills,name,' . $bill->id; - } - // is OK - $rules = [ - 'name' => $nameRule, + + return [ + 'name' => sprintf('required|between:1,255|uniqueObjectForUser:bills,name,%d', $bill->id), 'amount_min' => 'required|numeric|more:0|max:1000000000', 'amount_max' => 'required|numeric|more:0|max:1000000000', 'transaction_currency_id' => 'required|exists:transaction_currencies,id', @@ -85,7 +82,5 @@ class BillFormRequest extends Request 'skip' => 'required|between:0,31', 'active' => 'boolean', ]; - - return $rules; } } diff --git a/app/Models/Bill.php b/app/Models/Bill.php index 30697af841..fbd83bf4cc 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -161,6 +161,14 @@ class Bill extends Model return $this->morphMany(Note::class, 'noteable'); } + /** + * Get all of the tags for the post. + */ + public function objectGroups() + { + return $this->morphToMany(ObjectGroup::class, 'object_groupable'); + } + /** * @codeCoverageIgnore * diff --git a/app/Services/Internal/Update/BillUpdateService.php b/app/Services/Internal/Update/BillUpdateService.php index 42571b33c6..7df3070334 100644 --- a/app/Services/Internal/Update/BillUpdateService.php +++ b/app/Services/Internal/Update/BillUpdateService.php @@ -29,6 +29,7 @@ use FireflyIII\Models\Rule; use FireflyIII\Models\RuleTrigger; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; use FireflyIII\Services\Internal\Support\BillServiceTrait; use Illuminate\Support\Collection; use Log; @@ -39,7 +40,9 @@ use Log; */ class BillUpdateService { - use BillServiceTrait; + use BillServiceTrait, CreatesObjectGroups; + + protected $user; /** * Constructor. @@ -59,6 +62,7 @@ class BillUpdateService */ public function update(Bill $bill, array $data): Bill { + $this->user = $bill->user; /** @var TransactionCurrencyFactory $factory */ $factory = app(TransactionCurrencyFactory::class); /** @var TransactionCurrency $currency */ @@ -103,6 +107,25 @@ class BillUpdateService $this->updateBillActions($bill, $oldData['name'], $data['name']); $this->updateBillTriggers($bill, $oldData, $data); + // update using name: + $objectGroupTitle = $data['object_group'] ?? ''; + if ('' !== $objectGroupTitle) { + $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); + if (null !== $objectGroup) { + $bill->objectGroups()->sync([$objectGroup->id]); + $bill->save(); + } + } + // try also with ID: + $objectGroupId = (int) ($data['object_group_id'] ?? 0); + if (0 !== $objectGroupId) { + $objectGroup = $this->findObjectGroupById($objectGroupId); + if (null !== $objectGroup) { + $bill->objectGroups()->sync([$objectGroup->id]); + $bill->save(); + } + } + return $bill; } diff --git a/app/Transformers/BillTransformer.php b/app/Transformers/BillTransformer.php index 48503732fd..f2d4afbee4 100644 --- a/app/Transformers/BillTransformer.php +++ b/app/Transformers/BillTransformer.php @@ -25,6 +25,7 @@ namespace FireflyIII\Transformers; use Carbon\Carbon; use FireflyIII\Models\Bill; +use FireflyIII\Models\ObjectGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Support\Collection; @@ -67,6 +68,18 @@ class BillTransformer extends AbstractTransformer $notes = $this->repository->getNoteText($bill); $notes = '' === $notes ? null : $notes; $this->repository->setUser($bill->user); + + $objectGroupId = null; + $objectGroupOrder = null; + $objectGroupTitle = null; + /** @var ObjectGroup $objectGroup */ + $objectGroup = $bill->objectGroups->first(); + if (null !== $objectGroup) { + $objectGroupId = (int) $objectGroup->id; + $objectGroupOrder = (int) $objectGroup->order; + $objectGroupTitle = $objectGroup->title; + } + $data = [ 'id' => (int)$bill->id, 'created_at' => $bill->created_at->toAtomString(), @@ -86,6 +99,9 @@ class BillTransformer extends AbstractTransformer 'next_expected_match' => $paidData['next_expected_match'], 'pay_dates' => $payDates, 'paid_dates' => $paidData['paid_dates'], + 'object_group_id' => $objectGroupId, + 'object_group_order' => $objectGroupOrder, + 'object_group_title' => $objectGroupTitle, 'links' => [ [ 'rel' => 'self', diff --git a/resources/views/v1/bills/create.twig b/resources/views/v1/bills/create.twig index dcf45256f6..9ea39e3329 100644 --- a/resources/views/v1/bills/create.twig +++ b/resources/views/v1/bills/create.twig @@ -35,6 +35,7 @@ {{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }} {{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }} {{ ExpandedForm.integer('skip',0) }} + {{ ExpandedForm.objectGroup() }} @@ -72,4 +73,8 @@ + + {# auto complete for object groups #} + + {% endblock %} diff --git a/resources/views/v1/bills/edit.twig b/resources/views/v1/bills/edit.twig index 1a4f458eca..d916d702a4 100644 --- a/resources/views/v1/bills/edit.twig +++ b/resources/views/v1/bills/edit.twig @@ -41,6 +41,7 @@ {{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }} {{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }} {{ ExpandedForm.integer('skip') }} + {{ ExpandedForm.objectGroup() }} {# only correct way to do active checkbox #} {{ ExpandedForm.checkbox('active', 1) }} @@ -78,4 +79,8 @@ + + {# auto complete for object groups #} + + {% endblock %} diff --git a/resources/views/v1/bills/index.twig b/resources/views/v1/bills/index.twig index 475ada6b5f..d8d2e156be 100644 --- a/resources/views/v1/bills/index.twig +++ b/resources/views/v1/bills/index.twig @@ -5,7 +5,7 @@ {% endblock %} {% block content %} - {% if bills.count == 0 %} + {% if total == 0 %} {% include 'partials.empty' with {objectType: 'default', type: 'bills',route: route('bills.create')} %} {% else %}
{{ trans('list.name') }} | - -{{ trans('list.matchingAmount') }} | - - - + + +{{ trans('list.name') }} | + +{{ trans('list.matchingAmount') }} | + + +||||||
---|---|---|---|---|---|---|---|---|---|
- {% if not entry.active %} - - {% endif %} - {{ entry.name }} - {# count attachments #} - {% if entry.attachments.count > 0 %} - - {% endif %} + {% for objectGroupOrder, objectGroup in bills %} + {% if objectGroup.bills|length > 0 %} + | |||||||||
{{ objectGroup.object_group_title }} | +|||||||||
+ {% if not entry.active %} + + {% endif %} + {{ entry.name }} + {# count attachments #} + {% if entry.attachments.count > 0 %} + + {% endif %} - | - -+ | + +~ {{ formatAmountBySymbol((entry.amount_max + entry.amount_min)/2, entry.currency_symbol, entry.currency_decimal_places) }} - | + - {# - paidDates = 0 (bill not paid in period) - pay_dates = 0 (bill not expected to be paid in this period) - bill is active. - #} - {% if entry.paid_dates|length == 0 and entry.pay_dates|length == 0 and entry.active %} -- {{ trans('firefly.not_expected_period') }} - | - - {% endif %} + {# + paidDates = 0 (bill not paid in period) + pay_dates = 0 (bill not expected to be paid in this period) + bill is active. + #} + {% if entry.paid_dates|length == 0 and entry.pay_dates|length == 0 and entry.active %} ++ {{ trans('firefly.not_expected_period') }} + | + + {% endif %} - {# - paid_dates = 0 (bill not paid in period) - pay_dates > 0 (bill IS expected to be paid in this period) - bill is active - #} - {% if entry.paid_dates|length == 0 and entry.pay_dates|length > 0 and entry.active %} -- {{ trans('firefly.bill_expected_date', {date: entry.next_expected_match_diff }) }} - | -+ {{ trans('firefly.bill_expected_date', {date: entry.next_expected_match_diff }) }} + | + - {% endif %} + + {% endif %} - {# - paid_dates >= 0 (bill is paid X times). - Don't care about pay_dates. - #} - {% if entry.paid_dates|length > 0 and entry.active %} -
- {% for currentPaid in entry.paid_dates %}
-
- {{ formatDate(currentPaid.date, monthAndDayFormat) }}
-
- - {% endfor %} + {# + paid_dates >= 0 (bill is paid X times). + Don't care about pay_dates. + #} + {% if entry.paid_dates|length > 0 and entry.active %} + |
+ {% for currentPaid in entry.paid_dates %}
+
+ {{ formatDate(currentPaid.date, monthAndDayFormat) }}
+
+ + {% endfor %} + |
+
+ {% endif %}
+ {# bill is not active #}
+ {% if not entry.active %}
+ + ~ + | + + {% endif %} + +
+ {{ 'sum'|_ }} ({{ sum.currency_name }}) ({{ 'active_exp_bills_only'|_ }}) | -+ {{ formatAmountBySymbol(sum.avg, sum.currency_symbol, sum.currency_decimal_places) }} | - {% endif %} - {# bill is not active #} - {% if not entry.active %} -- ~ - | - - {% endif %} - -+ + + + {% endfor %} + + {% endif %} {% endfor %} - - - {% for sum in sums %} - | ||||||
- {{ 'sum'|_ }} ({{ sum.currency_name }}) ({{ 'active_exp_bills_only'|_ }}) - |
- - - {{ formatAmountBySymbol(sum.avg, sum.currency_symbol, sum.currency_decimal_places) }} - - | -- |