From 92ba56ae2b9f8e0865d1f5ea3651c65e1f2deab8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 13 Mar 2020 20:56:26 +0100 Subject: [PATCH 01/53] Clean up some forms. --- resources/views/v1/accounts/create.twig | 8 ++++---- resources/views/v1/accounts/edit.twig | 7 +++++-- resources/views/v1/accounts/reconcile/edit.twig | 5 ++++- resources/views/v1/admin/link/create.twig | 2 ++ resources/views/v1/admin/link/edit.twig | 2 ++ resources/views/v1/admin/users/edit.twig | 6 ++++-- resources/views/v1/attachments/edit.twig | 5 ++++- resources/views/v1/bills/create.twig | 5 ++++- resources/views/v1/bills/edit.twig | 6 +++++- resources/views/v1/budgets/create.twig | 6 ++++++ resources/views/v1/budgets/edit.twig | 8 +++++++- resources/views/v1/categories/create.twig | 3 ++- resources/views/v1/categories/edit.twig | 4 +++- resources/views/v1/currencies/create.twig | 3 ++- resources/views/v1/currencies/edit.twig | 5 +++-- resources/views/v1/piggy-banks/create.twig | 5 ++++- resources/views/v1/piggy-banks/edit.twig | 7 +++++-- resources/views/v1/recurring/create.twig | 2 +- resources/views/v1/recurring/edit.twig | 4 ++-- resources/views/v1/rules/rule-group/create.twig | 5 ++++- resources/views/v1/rules/rule-group/edit.twig | 7 +++++-- resources/views/v1/rules/rule/create.twig | 2 +- resources/views/v1/rules/rule/edit.twig | 4 ++-- resources/views/v1/tags/create.twig | 5 ++++- resources/views/v1/tags/edit.twig | 7 +++++-- 25 files changed, 90 insertions(+), 33 deletions(-) diff --git a/resources/views/v1/accounts/create.twig b/resources/views/v1/accounts/create.twig index 1b01a989b4..228f70233c 100644 --- a/resources/views/v1/accounts/create.twig +++ b/resources/views/v1/accounts/create.twig @@ -37,7 +37,6 @@
-

{{ 'optionalFields'|_ }}

@@ -61,8 +60,10 @@ {{ ExpandedForm.location('location', null, {locations: locations}) }}
- - +
+ +
+

{{ 'options'|_ }}

@@ -76,7 +77,6 @@
-
diff --git a/resources/views/v1/accounts/edit.twig b/resources/views/v1/accounts/edit.twig index 936edd1099..109e3a0a68 100644 --- a/resources/views/v1/accounts/edit.twig +++ b/resources/views/v1/accounts/edit.twig @@ -85,7 +85,10 @@ {% endif %} - + + +
+
{# panel for options #}
@@ -102,7 +105,7 @@
- {{ Form.close|raw }} + {% endblock %} {% block scripts %} diff --git a/resources/views/v1/accounts/reconcile/edit.twig b/resources/views/v1/accounts/reconcile/edit.twig index 9a02947a4d..575d274051 100644 --- a/resources/views/v1/accounts/reconcile/edit.twig +++ b/resources/views/v1/accounts/reconcile/edit.twig @@ -72,7 +72,10 @@
{% endif %} - + + +
+
{# panel for options #}
diff --git a/resources/views/v1/admin/link/create.twig b/resources/views/v1/admin/link/create.twig index 45272d6079..cf4b130715 100644 --- a/resources/views/v1/admin/link/create.twig +++ b/resources/views/v1/admin/link/create.twig @@ -21,6 +21,8 @@
+
+
{# panel for options #}
diff --git a/resources/views/v1/admin/link/edit.twig b/resources/views/v1/admin/link/edit.twig index 560c69188b..32ffb9a680 100644 --- a/resources/views/v1/admin/link/edit.twig +++ b/resources/views/v1/admin/link/edit.twig @@ -22,6 +22,8 @@
+ +
{# panel for options #} diff --git a/resources/views/v1/admin/users/edit.twig b/resources/views/v1/admin/users/edit.twig index bd4834c61e..7468734f2e 100644 --- a/resources/views/v1/admin/users/edit.twig +++ b/resources/views/v1/admin/users/edit.twig @@ -26,7 +26,9 @@
-
+
+
+
{# panel for options #}
@@ -43,5 +45,5 @@
- {{ Form.close|raw }} + {% endblock %} diff --git a/resources/views/v1/attachments/edit.twig b/resources/views/v1/attachments/edit.twig index d80bde0fba..e2bb7265b7 100644 --- a/resources/views/v1/attachments/edit.twig +++ b/resources/views/v1/attachments/edit.twig @@ -35,7 +35,10 @@ {{ ExpandedForm.textarea('notes',null,{helpText: trans('firefly.field_supports_markdown')}) }}
- + + +
+
{# panel for options #}
diff --git a/resources/views/v1/bills/create.twig b/resources/views/v1/bills/create.twig index bc299a9c54..dcf45256f6 100644 --- a/resources/views/v1/bills/create.twig +++ b/resources/views/v1/bills/create.twig @@ -37,7 +37,10 @@ {{ ExpandedForm.integer('skip',0) }}
- +
+
+
+
{# panel for options #}
diff --git a/resources/views/v1/bills/edit.twig b/resources/views/v1/bills/edit.twig index a1bdb1a002..216b6b7a40 100644 --- a/resources/views/v1/bills/edit.twig +++ b/resources/views/v1/bills/edit.twig @@ -42,6 +42,10 @@
+
+
+
+

{{ 'options'|_ }}

@@ -57,7 +61,7 @@
- {{ Form.close|raw }} + {% endblock %} {% block styles %} diff --git a/resources/views/v1/budgets/create.twig b/resources/views/v1/budgets/create.twig index 52966dd290..efb803bcbf 100644 --- a/resources/views/v1/budgets/create.twig +++ b/resources/views/v1/budgets/create.twig @@ -21,6 +21,12 @@
+
+ {# panel for auto-budget#} + abc +
+ +
{# panel for options #}
diff --git a/resources/views/v1/budgets/edit.twig b/resources/views/v1/budgets/edit.twig index f8c277b8a3..dd0c49b6a0 100644 --- a/resources/views/v1/budgets/edit.twig +++ b/resources/views/v1/budgets/edit.twig @@ -21,6 +21,12 @@
+
+ {# auto budget#} + bla bla +
+
+
{# panel for options #}
@@ -37,7 +43,7 @@
- {{ Form.close|raw }} + {% endblock %} {% block scripts %} diff --git a/resources/views/v1/categories/create.twig b/resources/views/v1/categories/create.twig index 4c969aabf2..770e93106e 100644 --- a/resources/views/v1/categories/create.twig +++ b/resources/views/v1/categories/create.twig @@ -19,7 +19,8 @@
- + +
{# panel for options #} diff --git a/resources/views/v1/categories/edit.twig b/resources/views/v1/categories/edit.twig index f308b77853..2d7f2c5ac6 100644 --- a/resources/views/v1/categories/edit.twig +++ b/resources/views/v1/categories/edit.twig @@ -19,6 +19,8 @@
+ +
@@ -38,7 +40,7 @@
- {{ Form.close|raw }} + {% endblock %} {% block scripts %} diff --git a/resources/views/v1/currencies/create.twig b/resources/views/v1/currencies/create.twig index 2524659508..bab224ea4d 100644 --- a/resources/views/v1/currencies/create.twig +++ b/resources/views/v1/currencies/create.twig @@ -22,7 +22,8 @@ - + +
{# panel for options #} diff --git a/resources/views/v1/currencies/edit.twig b/resources/views/v1/currencies/edit.twig index 1aec8f112c..221b226329 100644 --- a/resources/views/v1/currencies/edit.twig +++ b/resources/views/v1/currencies/edit.twig @@ -23,7 +23,8 @@
- + +
{# panel for options #} @@ -43,5 +44,5 @@
- {{ Form.close|raw }} + {% endblock %} diff --git a/resources/views/v1/piggy-banks/create.twig b/resources/views/v1/piggy-banks/create.twig index 06d3d5a181..8466c12749 100644 --- a/resources/views/v1/piggy-banks/create.twig +++ b/resources/views/v1/piggy-banks/create.twig @@ -34,7 +34,10 @@ {{ ExpandedForm.textarea('notes', null, {helpText: trans('firefly.field_supports_markdown')} ) }} - + + +
+
{# panel for options #}
diff --git a/resources/views/v1/piggy-banks/edit.twig b/resources/views/v1/piggy-banks/edit.twig index 668d1441a9..8dda406b0c 100644 --- a/resources/views/v1/piggy-banks/edit.twig +++ b/resources/views/v1/piggy-banks/edit.twig @@ -37,7 +37,10 @@ {{ ExpandedForm.textarea('notes', null, {helpText: trans('firefly.field_supports_markdown')}) }}
- +
+
+
+
{# panel for options #}
@@ -55,7 +58,7 @@
- {{ Form.close|raw }} + {% endblock %} {% block scripts %} diff --git a/resources/views/v1/recurring/create.twig b/resources/views/v1/recurring/create.twig index a02f0239d8..9de42801dd 100644 --- a/resources/views/v1/recurring/create.twig +++ b/resources/views/v1/recurring/create.twig @@ -140,7 +140,7 @@ {# row with submit stuff. #}
-
+

{{ 'options'|_ }}

diff --git a/resources/views/v1/recurring/edit.twig b/resources/views/v1/recurring/edit.twig index d06bf5e523..b241cd4efd 100644 --- a/resources/views/v1/recurring/edit.twig +++ b/resources/views/v1/recurring/edit.twig @@ -135,8 +135,8 @@
{# row with submit stuff. #} -
-
+
+

{{ 'options'|_ }}

diff --git a/resources/views/v1/rules/rule-group/create.twig b/resources/views/v1/rules/rule-group/create.twig index 8873384298..1ce63b0337 100644 --- a/resources/views/v1/rules/rule-group/create.twig +++ b/resources/views/v1/rules/rule-group/create.twig @@ -29,7 +29,10 @@ {{ ExpandedForm.textarea('description') }}
- +
+
+
+
{# panel for options #}
diff --git a/resources/views/v1/rules/rule-group/edit.twig b/resources/views/v1/rules/rule-group/edit.twig index 3071059ef6..5a89eb509f 100644 --- a/resources/views/v1/rules/rule-group/edit.twig +++ b/resources/views/v1/rules/rule-group/edit.twig @@ -33,7 +33,10 @@ {{ ExpandedForm.textarea('description', ruleGroup.description) }}
- +
+
+
+
{# panel for options #}
@@ -49,7 +52,7 @@
- {{ Form.close|raw }} + {% endblock %} {% block scripts %} diff --git a/resources/views/v1/rules/rule/create.twig b/resources/views/v1/rules/rule/create.twig index 3d9373c2c9..cb3e0acc28 100644 --- a/resources/views/v1/rules/rule/create.twig +++ b/resources/views/v1/rules/rule/create.twig @@ -124,7 +124,7 @@
-
+
{# panel for options #}
diff --git a/resources/views/v1/rules/rule/edit.twig b/resources/views/v1/rules/rule/edit.twig index 039697de70..451496da71 100644 --- a/resources/views/v1/rules/rule/edit.twig +++ b/resources/views/v1/rules/rule/edit.twig @@ -106,7 +106,7 @@
-
+
{# panel for options #}
@@ -123,7 +123,7 @@
- {{ Form.close|raw }} + {% endblock %} diff --git a/resources/views/v1/tags/create.twig b/resources/views/v1/tags/create.twig index e53abf4d9f..30f5a4ce51 100644 --- a/resources/views/v1/tags/create.twig +++ b/resources/views/v1/tags/create.twig @@ -36,7 +36,10 @@ {{ ExpandedForm.location('location', null, {locations: locations}) }}
- +
+
+
+
{# panel for options #}
diff --git a/resources/views/v1/tags/edit.twig b/resources/views/v1/tags/edit.twig index 1c7a6b1819..e69ddfc3bd 100644 --- a/resources/views/v1/tags/edit.twig +++ b/resources/views/v1/tags/edit.twig @@ -39,7 +39,10 @@ {{ ExpandedForm.location('location', null, {locations: locations}) }}
- +
+
+
+
{# panel for options #}
@@ -58,7 +61,7 @@
- {{ Form.close|raw }} + {% endblock %} {% block scripts %} From 81d7b7b6a16c613111545d256a1925398b0adbfe Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 13 Mar 2020 21:15:21 +0100 Subject: [PATCH 02/53] New model and migration. --- app/Models/AutoBudget.php | 39 ++++++++++++++ .../2020_03_13_201950_changes_for_v520.php | 54 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 app/Models/AutoBudget.php create mode 100644 database/migrations/2020_03_13_201950_changes_for_v520.php diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php new file mode 100644 index 0000000000..882b9c6ed8 --- /dev/null +++ b/app/Models/AutoBudget.php @@ -0,0 +1,39 @@ +. + */ + +namespace FireflyIII; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; + +/** + * Class AutoBudget + */ +class AutoBudget extends Model +{ + /** @var int When the auto-budget resets every period automatically. */ + public const AUTO_BUDGET_RESET = 1; + /** @var int When the auto-budget adds an amount every period automatically */ + public const AUTO_BUDGET_ROLLOVER = 2; + + use SoftDeletes; + // +} diff --git a/database/migrations/2020_03_13_201950_changes_for_v520.php b/database/migrations/2020_03_13_201950_changes_for_v520.php new file mode 100644 index 0000000000..4d7899454f --- /dev/null +++ b/database/migrations/2020_03_13_201950_changes_for_v520.php @@ -0,0 +1,54 @@ +increments('id'); + $table->timestamps(); + $table->softDeletes(); + $table->integer('budget_id', false, true); + $table->integer('transaction_currency_id', false, true); + $table->tinyInteger('auto_budget_type', false, true)->default(1); + $table->decimal('amount', 22, 12); + $table->string('period', 50); + + + //$table->string('password', 60); + //$table->string('remember_token', 100)->nullable(); + //$table->string('reset', 32)->nullable(); + //$table->tinyInteger('blocked', false, true)->default('0'); + //$table->string('blocked_code', 25)->nullable(); + + $table->foreign('transaction_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade'); + $table->foreign('budget_id')->references('id')->on('budgets')->onDelete('cascade'); + } + ); + } + } +} From cd65971f5edcee85fdd473eacc5af2f6ff3df2ad Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 13 Mar 2020 21:15:37 +0100 Subject: [PATCH 03/53] Some new strings for new model. --- resources/lang/en_US/firefly.php | 9 +++++++++ resources/lang/en_US/form.php | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 9093c48937..7234877b75 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -783,6 +783,15 @@ return [ 'over_budget_warn' => ' Usually you budget about :amount per day. This time it\'s :over_amount per day. Are you sure?', 'transferred_in' => 'Transferred (in)', 'transferred_away' => 'Transferred (away)', + 'auto_budget_none' => 'No auto-budget', + 'auto_budget_reset' => 'Set a fixed amount every period', + 'auto_budget_rollover' => 'Add an amount every period', + 'auto_budget_period_daily' => 'Daily', + 'auto_budget_period_weekly' => 'Weekly', + 'auto_budget_period_monthly' => 'Monthly', + 'auto_budget_period_quarterly' => 'Quarterly', + 'auto_budget_period_half_year' => 'Every half year', + 'auto_budget_period_yearly' => 'Yearly', // bills: 'match_between_amounts' => 'Bill matches transactions between :low and :high.', diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index 7b994976c6..73952ff96f 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -260,4 +260,9 @@ return [ 'expected_on' => 'Expected on', 'paid' => 'Paid', + 'auto_budget_option' => 'Auto-budget', + 'auto_budget_amount' => 'Auto-budget amount', + 'auto_budget_period' => 'Auto-budget period', + + ]; From 6f98fc0dffed2f9d77b3e3858854ca018de72913 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 13 Mar 2020 21:15:54 +0100 Subject: [PATCH 04/53] First attempt at form. --- .../Controllers/Budget/CreateController.php | 26 ++++++++++++++++++- resources/views/v1/budgets/create.twig | 16 +++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Budget/CreateController.php b/app/Http/Controllers/Budget/CreateController.php index 6418b8251f..8230eb333f 100644 --- a/app/Http/Controllers/Budget/CreateController.php +++ b/app/Http/Controllers/Budget/CreateController.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Budget; +use FireflyIII\AutoBudget; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\BudgetFormRequest; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; @@ -68,6 +69,29 @@ class CreateController extends Controller */ public function create(Request $request) { + $hasOldInput = null !== $request->old('_token'); + + // auto budget options + $autoBudgetOptions = [ + 0 => (string)trans('firefly.auto_budget_none'), + AutoBudget::AUTO_BUDGET_RESET => (string)trans('firefly.auto_budget_reset'), + AutoBudget::AUTO_BUDGET_ROLLOVER => (string)trans('firefly.auto_budget_rollover'), + ]; + $autoBudgetPeriods = [ + 'daily' => (string)trans('firefly.auto_budget_period_daily'), + 'weekly' => (string)trans('firefly.auto_budget_period_weekly'), + 'monthly' => (string)trans('firefly.auto_budget_period_monthly'), + 'quarterly' => (string)trans('firefly.auto_budget_period_quarterly'), + 'half_year' => (string)trans('firefly.auto_budget_period_half_year'), + 'yearly' => (string)trans('firefly.auto_budget_period_yearly'), + ]; + + $preFilled = [ + 'auto_budget_period' => $hasOldInput ? (bool)$request->old('auto_budget_period') : 'monthly', + ]; + + $request->session()->flash('preFilled', $preFilled); + // put previous url in session if not redirect from store (not "create another"). if (true !== session('budgets.create.fromStore')) { $this->rememberPreviousUri('budgets.create.uri'); @@ -75,7 +99,7 @@ class CreateController extends Controller $request->session()->forget('budgets.create.fromStore'); $subTitle = (string)trans('firefly.create_new_budget'); - return view('budgets.create', compact('subTitle')); + return view('budgets.create', compact('subTitle', 'autoBudgetOptions', 'autoBudgetPeriods')); } diff --git a/resources/views/v1/budgets/create.twig b/resources/views/v1/budgets/create.twig index efb803bcbf..7b78db5cdb 100644 --- a/resources/views/v1/budgets/create.twig +++ b/resources/views/v1/budgets/create.twig @@ -23,7 +23,21 @@
{# panel for auto-budget#} - abc +
+
+

{{ 'optionalFields'|_ }}

+
+
+ {{ ExpandedForm.select('auto_budget_option', autoBudgetOptions) }} + {{ CurrencyForm.currencyList('transaction_currency_id') }} + {{ ExpandedForm.amountNoCurrency('auto_budget_amount') }} + {{ ExpandedForm.select('auto_budget_period', autoBudgetPeriods, null) }} +
+ +
+
From 2ece754927f3fecae3133643bc38bc4ca7eed8a1 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 13 Mar 2020 21:35:22 +0100 Subject: [PATCH 05/53] Can now store and validate auto budget --- .../Controllers/Budget/CreateController.php | 7 +- .../Controllers/Budget/EditController.php | 8 +- app/Http/Requests/BudgetFormStoreRequest.php | 116 ++++++++++++++++++ ...equest.php => BudgetFormUpdateRequest.php} | 6 +- app/Models/AutoBudget.php | 12 +- app/Models/Budget.php | 11 ++ app/Repositories/Budget/BudgetRepository.php | 17 ++- resources/lang/en_US/validation.php | 4 + 8 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 app/Http/Requests/BudgetFormStoreRequest.php rename app/Http/Requests/{BudgetFormRequest.php => BudgetFormUpdateRequest.php} (94%) diff --git a/app/Http/Controllers/Budget/CreateController.php b/app/Http/Controllers/Budget/CreateController.php index 8230eb333f..81dd47e09b 100644 --- a/app/Http/Controllers/Budget/CreateController.php +++ b/app/Http/Controllers/Budget/CreateController.php @@ -26,7 +26,7 @@ namespace FireflyIII\Http\Controllers\Budget; use FireflyIII\AutoBudget; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Http\Requests\BudgetFormRequest; +use FireflyIII\Http\Requests\BudgetFormStoreRequest; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -106,13 +106,14 @@ class CreateController extends Controller /** * Stores a budget. * - * @param BudgetFormRequest $request + * @param BudgetFormStoreRequest $request * * @return \Illuminate\Http\RedirectResponse */ - public function store(BudgetFormRequest $request): RedirectResponse + public function store(BudgetFormStoreRequest $request): RedirectResponse { $data = $request->getBudgetData(); + $budget = $this->repository->store($data); $this->repository->cleanupBudgets(); $request->session()->flash('success', (string)trans('firefly.stored_new_budget', ['name' => $budget->name])); diff --git a/app/Http/Controllers/Budget/EditController.php b/app/Http/Controllers/Budget/EditController.php index f7af796cd7..91125c6e4d 100644 --- a/app/Http/Controllers/Budget/EditController.php +++ b/app/Http/Controllers/Budget/EditController.php @@ -25,7 +25,7 @@ namespace FireflyIII\Http\Controllers\Budget; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Http\Requests\BudgetFormRequest; +use FireflyIII\Http\Requests\BudgetFormUpdateRequest; use FireflyIII\Models\Budget; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use Illuminate\Http\RedirectResponse; @@ -90,12 +90,12 @@ class EditController extends Controller /** * Budget update routine. * - * @param BudgetFormRequest $request - * @param Budget $budget + * @param BudgetFormUpdateRequest $request + * @param Budget $budget * * @return \Illuminate\Http\RedirectResponse */ - public function update(BudgetFormRequest $request, Budget $budget): RedirectResponse + public function update(BudgetFormUpdateRequest $request, Budget $budget): RedirectResponse { $data = $request->getBudgetData(); $this->repository->update($budget, $data); diff --git a/app/Http/Requests/BudgetFormStoreRequest.php b/app/Http/Requests/BudgetFormStoreRequest.php new file mode 100644 index 0000000000..f7287c4ff3 --- /dev/null +++ b/app/Http/Requests/BudgetFormStoreRequest.php @@ -0,0 +1,116 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Http\Requests; + +use FireflyIII\AutoBudget; +use Illuminate\Validation\Validator; + +/** + * @codeCoverageIgnore + * Class BudgetFormStoreRequest + */ +class BudgetFormStoreRequest extends Request +{ + /** + * Verify the request. + * + * @return bool + */ + public function authorize(): bool + { + return auth()->check(); + } + + /** + * Returns the data required by the controller. + * + * @return array + */ + public function getBudgetData(): array + { + return [ + 'name' => $this->string('name'), + 'active' => $this->boolean('active'), + 'auto_budget_option' => $this->integer('auto_budget_option'), + 'transaction_currency_id' => $this->integer('transaction_currency_id'), + 'auto_budget_amount' => $this->string('auto_budget_amount'), + 'auto_budget_period' => $this->string('auto_budget_period'), + ]; + } + + /** + * Rules for this request. + * + * @return array + */ + public function rules(): array + { + return [ + 'name' => 'required|between:1,100|uniqueObjectForUser:budgets,name', + 'active' => 'numeric|between:0,1', + 'auto_budget_option' => 'numeric|between:0,2', + 'transaction_currency_id' => 'required|exists:transaction_currencies,id', + 'auto_budget_amount' => 'min:0|max:1000000000', + 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', + ]; + } + + /** + * Configure the validator instance with special rules for after the basic validation rules. + * + * @param Validator $validator + * + * @return void + */ + public function withValidator(Validator $validator): void + { + $validator->after( + function (Validator $validator) { + // validate all account info + $this->validateAmount($validator); + } + ); + } + + /** + * @param Validator $validator + */ + private function validateAmount(Validator $validator): void + { + $data = $validator->getData(); + $option = (int)$data['auto_budget_option']; + $amount = $data['auto_budget_amount'] ?? ''; + switch ($option) { + case AutoBudget::AUTO_BUDGET_RESET: + case AutoBudget::AUTO_BUDGET_ROLLOVER: + // basic float check: + if ('' === $amount) { + $validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget')); + } + if (1 !== bccomp((string)$amount, '0')) { + $validator->errors()->add('auto_budget_amount', (string)trans('validation.auto_budget_amount_positive')); + } + break; + } + } +} diff --git a/app/Http/Requests/BudgetFormRequest.php b/app/Http/Requests/BudgetFormUpdateRequest.php similarity index 94% rename from app/Http/Requests/BudgetFormRequest.php rename to app/Http/Requests/BudgetFormUpdateRequest.php index 4d0924e7c1..ed01f1ee6c 100644 --- a/app/Http/Requests/BudgetFormRequest.php +++ b/app/Http/Requests/BudgetFormUpdateRequest.php @@ -25,12 +25,10 @@ namespace FireflyIII\Http\Requests; use FireflyIII\Models\Budget; /** - * Class BudgetFormRequest. - * * @codeCoverageIgnore - * TODO AFTER 4.8,0, split for update/store + * Class BudgetFormUpdateRequest */ -class BudgetFormRequest extends Request +class BudgetFormUpdateRequest extends Request { /** * Verify the request. diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php index 882b9c6ed8..fef108460d 100644 --- a/app/Models/AutoBudget.php +++ b/app/Models/AutoBudget.php @@ -21,7 +21,9 @@ namespace FireflyIII; +use FireflyIII\Models\Budget; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; /** @@ -35,5 +37,13 @@ class AutoBudget extends Model public const AUTO_BUDGET_ROLLOVER = 2; use SoftDeletes; - // + + /** + * @codeCoverageIgnore + * @return BelongsTo + */ + public function budget(): BelongsTo + { + return $this->belongsTo(Budget::class); + } } diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 41b81292ee..298a7c7475 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; use Carbon\Carbon; +use FireflyIII\AutoBudget; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -122,6 +123,16 @@ class Budget extends Model return $this->hasMany(BudgetLimit::class); } + + /** + * @codeCoverageIgnore + * @return HasMany + */ + public function autoBudgets(): HasMany + { + return $this->hasMany(AutoBudget::class); + } + /** * @codeCoverageIgnore * @return BelongsToMany diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index c6c90a432c..1ac9eca663 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -25,6 +25,7 @@ namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; use DB; use Exception; +use FireflyIII\AutoBudget; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; @@ -282,10 +283,24 @@ class BudgetRepository implements BudgetRepositoryInterface 'name' => $data['name'], ] ); - } catch(QueryException $e) { + } catch (QueryException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); throw new FireflyException('400002: Could not store budget.'); } + // try to create associated auto budget: + $option = $data['auto_budget_option'] ?? 0; + if (0 === $option) { + return $newBudget; + } + $autoBudget = new AutoBudget; + $autoBudget->budget()->associate($newBudget); + $autoBudget->transaction_currency_id = $data['transaction_currency_id'] ?? 1; + $autoBudget->auto_budget_type = $option; + $autoBudget->amount = $data['auto_budget_amount'] ?? '1'; + $autoBudget->period = $data['auto_budget_period'] ?? 'monthly'; + $autoBudget->save(); return $newBudget; } diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index f339d2502e..caaed38148 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -130,6 +130,7 @@ return [ 'amount_zero' => 'The total amount cannot be zero.', 'current_target_amount' => 'The current amount must be less than the target amount.', 'unique_piggy_bank_for_user' => 'The name of the piggy bank must be unique.', + 'secure_password' => 'This is not a secure password. Please try again. For more information, visit https://bit.ly/FF3-password-security', 'valid_recurrence_rep_type' => 'Invalid repetition type for recurring transactions.', 'valid_recurrence_rep_moment' => 'Invalid repetition moment for this type of repetition.', @@ -200,4 +201,7 @@ return [ 'gte.file' => 'The :attribute must be greater than or equal to :value kilobytes.', 'gte.string' => 'The :attribute must be greater than or equal to :value characters.', 'gte.array' => 'The :attribute must have :value items or more.', + + 'amount_required_for_auto_budget' => 'The amount is required.', + 'auto_budget_amount_positive' => 'The amount must be more than zero.', ]; From 309633069ceb027a254a9f9ef8da06b578ca6585 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 07:01:31 +0100 Subject: [PATCH 06/53] can edit, delete and see in api autobudget --- .../Controllers/Budget/CreateController.php | 6 ++- .../Controllers/Budget/EditController.php | 29 +++++++++-- app/Http/Requests/BudgetFormStoreRequest.php | 24 +-------- app/Http/Requests/BudgetFormUpdateRequest.php | 34 +++++++++++-- app/Http/Requests/Request.php | 24 +++++++++ app/Models/AutoBudget.php | 12 ++++- app/Models/Budget.php | 1 - app/Models/RecurrenceTransaction.php | 1 - app/Repositories/Budget/BudgetRepository.php | 26 +++++++++- .../Budget/BudgetRepositoryInterface.php | 8 +++ .../Internal/Destroy/BudgetDestroyService.php | 6 +++ app/Transformers/BudgetTransformer.php | 49 +++++++++++++++---- resources/views/v1/budgets/create.twig | 4 -- resources/views/v1/budgets/edit.twig | 14 +++++- 14 files changed, 185 insertions(+), 53 deletions(-) diff --git a/app/Http/Controllers/Budget/CreateController.php b/app/Http/Controllers/Budget/CreateController.php index 81dd47e09b..d7b79673d8 100644 --- a/app/Http/Controllers/Budget/CreateController.php +++ b/app/Http/Controllers/Budget/CreateController.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Budget; -use FireflyIII\AutoBudget; +use FireflyIII\Models\AutoBudget; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\BudgetFormStoreRequest; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; @@ -85,9 +85,11 @@ class CreateController extends Controller 'half_year' => (string)trans('firefly.auto_budget_period_half_year'), 'yearly' => (string)trans('firefly.auto_budget_period_yearly'), ]; + $currency = app('amount')->getDefaultCurrency(); $preFilled = [ - 'auto_budget_period' => $hasOldInput ? (bool)$request->old('auto_budget_period') : 'monthly', + 'auto_budget_period' => $hasOldInput ? (bool)$request->old('auto_budget_period') : 'monthly', + 'transaction_currency_id' => $hasOldInput ? (int)$request->old('transaction_currency_id') : $currency->id, ]; $request->session()->flash('preFilled', $preFilled); diff --git a/app/Http/Controllers/Budget/EditController.php b/app/Http/Controllers/Budget/EditController.php index 91125c6e4d..536f827c73 100644 --- a/app/Http/Controllers/Budget/EditController.php +++ b/app/Http/Controllers/Budget/EditController.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Budget; +use FireflyIII\Models\AutoBudget; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\BudgetFormUpdateRequest; use FireflyIII\Models\Budget; @@ -69,13 +70,35 @@ class EditController extends Controller */ public function edit(Request $request, Budget $budget) { - $subTitle = (string)trans('firefly.edit_budget', ['name' => $budget->name]); + $subTitle = (string)trans('firefly.edit_budget', ['name' => $budget->name]); + $autoBudget = $this->repository->getAutoBudget($budget); + // auto budget options + $autoBudgetOptions = [ + 0 => (string)trans('firefly.auto_budget_none'), + AutoBudget::AUTO_BUDGET_RESET => (string)trans('firefly.auto_budget_reset'), + AutoBudget::AUTO_BUDGET_ROLLOVER => (string)trans('firefly.auto_budget_rollover'), + ]; + $autoBudgetPeriods = [ + 'daily' => (string)trans('firefly.auto_budget_period_daily'), + 'weekly' => (string)trans('firefly.auto_budget_period_weekly'), + 'monthly' => (string)trans('firefly.auto_budget_period_monthly'), + 'quarterly' => (string)trans('firefly.auto_budget_period_quarterly'), + 'half_year' => (string)trans('firefly.auto_budget_period_half_year'), + 'yearly' => (string)trans('firefly.auto_budget_period_yearly'), + ]; // code to handle active-checkboxes $hasOldInput = null !== $request->old('_token'); $preFilled = [ - 'active' => $hasOldInput ? (bool)$request->old('active') : $budget->active, + 'active' => $hasOldInput ? (bool)$request->old('active') : $budget->active, ]; + if($autoBudget) { + $preFilled['auto_budget_amount'] = $hasOldInput ? $request->old('auto_budget_amount') : $autoBudget->amount; + //'auto_budget_option' => $request->, + //'transaction_currency_id' => 'required|exists:transaction_currencies,id', + //'auto_budget_amount' => $request->old('auto_budget_amount'), + //'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', + } // put previous url in session if not redirect from store (not "return_to_edit"). if (true !== session('budgets.edit.fromUpdate')) { @@ -84,7 +107,7 @@ class EditController extends Controller $request->session()->forget('budgets.edit.fromUpdate'); $request->session()->flash('preFilled', $preFilled); - return view('budgets.edit', compact('budget', 'subTitle')); + return view('budgets.edit', compact('budget', 'subTitle', 'autoBudgetOptions', 'autoBudgetPeriods', 'autoBudget')); } /** diff --git a/app/Http/Requests/BudgetFormStoreRequest.php b/app/Http/Requests/BudgetFormStoreRequest.php index f7287c4ff3..9e27e337f1 100644 --- a/app/Http/Requests/BudgetFormStoreRequest.php +++ b/app/Http/Requests/BudgetFormStoreRequest.php @@ -22,7 +22,6 @@ declare(strict_types=1); namespace FireflyIII\Http\Requests; -use FireflyIII\AutoBudget; use Illuminate\Validation\Validator; /** @@ -87,30 +86,9 @@ class BudgetFormStoreRequest extends Request $validator->after( function (Validator $validator) { // validate all account info - $this->validateAmount($validator); + $this->validateAutoBudgetAmount($validator); } ); } - /** - * @param Validator $validator - */ - private function validateAmount(Validator $validator): void - { - $data = $validator->getData(); - $option = (int)$data['auto_budget_option']; - $amount = $data['auto_budget_amount'] ?? ''; - switch ($option) { - case AutoBudget::AUTO_BUDGET_RESET: - case AutoBudget::AUTO_BUDGET_ROLLOVER: - // basic float check: - if ('' === $amount) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget')); - } - if (1 !== bccomp((string)$amount, '0')) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.auto_budget_amount_positive')); - } - break; - } - } } diff --git a/app/Http/Requests/BudgetFormUpdateRequest.php b/app/Http/Requests/BudgetFormUpdateRequest.php index ed01f1ee6c..d9c54cfb5f 100644 --- a/app/Http/Requests/BudgetFormUpdateRequest.php +++ b/app/Http/Requests/BudgetFormUpdateRequest.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Requests; use FireflyIII\Models\Budget; +use Illuminate\Validation\Validator; /** * @codeCoverageIgnore @@ -48,8 +49,12 @@ class BudgetFormUpdateRequest extends Request public function getBudgetData(): array { return [ - 'name' => $this->string('name'), - 'active' => $this->boolean('active'), + 'name' => $this->string('name'), + 'active' => $this->boolean('active'), + 'auto_budget_option' => $this->integer('auto_budget_option'), + 'transaction_currency_id' => $this->integer('transaction_currency_id'), + 'auto_budget_amount' => $this->string('auto_budget_amount'), + 'auto_budget_period' => $this->string('auto_budget_period'), ]; } @@ -70,8 +75,29 @@ class BudgetFormUpdateRequest extends Request } return [ - 'name' => $nameRule, - 'active' => 'numeric|between:0,1', + 'name' => $nameRule, + 'active' => 'numeric|between:0,1', + 'auto_budget_option' => 'numeric|between:0,2', + 'transaction_currency_id' => 'required|exists:transaction_currencies,id', + 'auto_budget_amount' => 'min:0|max:1000000000', + 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', ]; } + + /** + * Configure the validator instance with special rules for after the basic validation rules. + * + * @param Validator $validator + * + * @return void + */ + public function withValidator(Validator $validator): void + { + $validator->after( + function (Validator $validator) { + // validate all account info + $this->validateAutoBudgetAmount($validator); + } + ); + } } diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index d85310ff38..f98e2e0b61 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -25,7 +25,9 @@ namespace FireflyIII\Http\Requests; use Carbon\Carbon; use Carbon\Exceptions\InvalidDateException; use Exception; +use FireflyIII\Models\AutoBudget; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Validator; use Log; /** @@ -385,4 +387,26 @@ class Request extends FormRequest return $data; } + /** + * @param Validator $validator + */ + protected function validateAutoBudgetAmount(Validator $validator): void + { + $data = $validator->getData(); + $option = (int)$data['auto_budget_option']; + $amount = $data['auto_budget_amount'] ?? ''; + switch ($option) { + case AutoBudget::AUTO_BUDGET_RESET: + case AutoBudget::AUTO_BUDGET_ROLLOVER: + // basic float check: + if ('' === $amount) { + $validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget')); + } + if (1 !== bccomp((string)$amount, '0')) { + $validator->errors()->add('auto_budget_amount', (string)trans('validation.auto_budget_amount_positive')); + } + break; + } + } + } diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php index fef108460d..4d9dd6c834 100644 --- a/app/Models/AutoBudget.php +++ b/app/Models/AutoBudget.php @@ -19,9 +19,8 @@ * along with this program. If not, see . */ -namespace FireflyIII; +namespace FireflyIII\Models; -use FireflyIII\Models\Budget; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; @@ -46,4 +45,13 @@ class AutoBudget extends Model { return $this->belongsTo(Budget::class); } + + /** + * @codeCoverageIgnore + * @return BelongsTo + */ + public function transactionCurrency(): BelongsTo + { + return $this->belongsTo(TransactionCurrency::class); + } } diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 298a7c7475..a9b44ba18a 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace FireflyIII\Models; use Carbon\Carbon; -use FireflyIII\AutoBudget; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; diff --git a/app/Models/RecurrenceTransaction.php b/app/Models/RecurrenceTransaction.php index 87ad759a8f..cedd49f236 100644 --- a/app/Models/RecurrenceTransaction.php +++ b/app/Models/RecurrenceTransaction.php @@ -50,7 +50,6 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at * @property int $recurrence_id - * @property int $transaction_currency_id * @method static bool|null forceDelete() * @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\RecurrenceTransaction newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|\FireflyIII\Models\RecurrenceTransaction newQuery() diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 1ac9eca663..e4ae35eeec 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -25,7 +25,7 @@ namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; use DB; use Exception; -use FireflyIII\AutoBudget; +use FireflyIII\Models\AutoBudget; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; @@ -316,6 +316,22 @@ class BudgetRepository implements BudgetRepositoryInterface $budget->name = $data['name']; $budget->active = $data['active']; $budget->save(); + + // update or create auto-budget: + $autoBudgetType = $data['auto_budget_option'] ?? 0; + if (0 !== $autoBudgetType) { + $autoBudget = $this->getAutoBudget($budget); + if (null === $autoBudget) { + $autoBudget = new AutoBudget; + $autoBudget->budget()->associate($budget); + } + $autoBudget->transaction_currency_id = $data['transaction_currency_id'] ?? 1; + $autoBudget->auto_budget_type = $autoBudgetType; + $autoBudget->amount = $data['auto_budget_amount'] ?? '0'; + $autoBudget->period = $data['auto_budget_period'] ?? 'monthly'; + $autoBudget->save(); + } + $this->updateRuleTriggers($oldName, $data['name']); $this->updateRuleActions($oldName, $data['name']); app('preferences')->mark(); @@ -380,4 +396,12 @@ class BudgetRepository implements BudgetRepositoryInterface $budget->delete(); } } + + /** + * @inheritDoc + */ + public function getAutoBudget(Budget $budget): ?AutoBudget + { + return $budget->autoBudgets()->first(); + } } diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index b1bf472bc9..a58d29a96f 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; +use FireflyIII\Models\AutoBudget; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Budget; use FireflyIII\User; @@ -38,6 +39,13 @@ interface BudgetRepositoryInterface */ public function destroyAll(): void; + /** + * @param Budget $budget + * + * @return AutoBudget|null + */ + public function getAutoBudget(Budget $budget): ?AutoBudget; + /** * @return bool diff --git a/app/Services/Internal/Destroy/BudgetDestroyService.php b/app/Services/Internal/Destroy/BudgetDestroyService.php index f1573ab384..745c4886fd 100644 --- a/app/Services/Internal/Destroy/BudgetDestroyService.php +++ b/app/Services/Internal/Destroy/BudgetDestroyService.php @@ -49,12 +49,18 @@ class BudgetDestroyService */ public function destroy(Budget $budget): void { + try { $budget->delete(); } catch (Exception $e) { // @codeCoverageIgnore Log::error(sprintf('Could not delete budget: %s', $e->getMessage())); // @codeCoverageIgnore } + // also delete auto budget: + foreach ($budget->autoBudgets()->get() as $autoBudget) { + $autoBudget->delete(); + } + // also delete all relations between categories and transaction journals: DB::table('budget_transaction_journal')->where('budget_id', (int)$budget->id)->delete(); diff --git a/app/Transformers/BudgetTransformer.php b/app/Transformers/BudgetTransformer.php index 2f4e8eedda..245683107a 100644 --- a/app/Transformers/BudgetTransformer.php +++ b/app/Transformers/BudgetTransformer.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; +use FireflyIII\Models\AutoBudget; use FireflyIII\Models\Budget; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; @@ -37,6 +38,8 @@ class BudgetTransformer extends AbstractTransformer { /** @var OperationsRepositoryInterface */ private $opsRepository; + /** @var BudgetRepositoryInterface */ + private $repository; /** * BudgetTransformer constructor. @@ -46,6 +49,7 @@ class BudgetTransformer extends AbstractTransformer public function __construct() { $this->opsRepository = app(OperationsRepositoryInterface::class); + $this->repository = app(BudgetRepositoryInterface::class); if ('testing' === config('app.env')) { Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this))); } @@ -61,21 +65,46 @@ class BudgetTransformer extends AbstractTransformer public function transform(Budget $budget): array { $this->opsRepository->setUser($budget->user); - $start = $this->parameters->get('start'); - $end = $this->parameters->get('end'); - $spent = []; + $start = $this->parameters->get('start'); + $end = $this->parameters->get('end'); + $autoBudget = $this->repository->getAutoBudget($budget); + $spent = []; if (null !== $start && null !== $end) { $spent = array_values($this->opsRepository->sumExpenses($start, $end, null, new Collection([$budget]))); } + $abCurrencyId = null; + $abCurrencyCode = null; + $abType = null; + $abAmount = null; + $abPeriod = null; + + $types = [ + AutoBudget::AUTO_BUDGET_RESET => 'reset', + AutoBudget::AUTO_BUDGET_ROLLOVER => 'rollover', + ]; + + if (null !== $autoBudget) { + $abCurrencyId = $autoBudget->transactionCurrency->id; + $abCurrencyCode = $autoBudget->transactionCurrency->code; + $abType = $types[$autoBudget->auto_budget_type]; + $abAmount = $autoBudget->amount; + $abPeriod = $autoBudget->period; + } + $data = [ - 'id' => (int)$budget->id, - 'created_at' => $budget->created_at->toAtomString(), - 'updated_at' => $budget->updated_at->toAtomString(), - 'active' => $budget->active, - 'name' => $budget->name, - 'spent' => $spent, - 'links' => [ + 'id' => (int)$budget->id, + 'created_at' => $budget->created_at->toAtomString(), + 'updated_at' => $budget->updated_at->toAtomString(), + 'active' => $budget->active, + 'name' => $budget->name, + 'auto_budget_currency_id' => $abCurrencyId, + 'auto_budget_currency_code' => $abCurrencyCode, + 'auto_budget_type' => $abType, + 'auto_budget_amount' => $abAmount, + 'auto_budget_period' => $abPeriod, + 'spent' => $spent, + 'links' => [ [ 'rel' => 'self', 'uri' => '/budgets/' . $budget->id, diff --git a/resources/views/v1/budgets/create.twig b/resources/views/v1/budgets/create.twig index 7b78db5cdb..91713d29e8 100644 --- a/resources/views/v1/budgets/create.twig +++ b/resources/views/v1/budgets/create.twig @@ -33,11 +33,7 @@ {{ ExpandedForm.amountNoCurrency('auto_budget_amount') }} {{ ExpandedForm.select('auto_budget_period', autoBudgetPeriods, null) }}
-
-
diff --git a/resources/views/v1/budgets/edit.twig b/resources/views/v1/budgets/edit.twig index dd0c49b6a0..4b64626a92 100644 --- a/resources/views/v1/budgets/edit.twig +++ b/resources/views/v1/budgets/edit.twig @@ -22,8 +22,18 @@
- {# auto budget#} - bla bla + {# panel for auto-budget #} +
+
+

{{ 'optionalFields'|_ }}

+
+
+ {{ ExpandedForm.select('auto_budget_option', autoBudgetOptions, autoBudget.auto_budget_type) }} + {{ CurrencyForm.currencyList('transaction_currency_id', autoBudget.transaction_currency_id) }} + {{ ExpandedForm.amountNoCurrency('auto_budget_amount', preFilled.auto_budget_amount) }} + {{ ExpandedForm.select('auto_budget_period', autoBudgetPeriods, autoBudget.period) }} +
+
From d1d11ae717d887919a46e5c6cd2a56715f84637f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 07:30:55 +0100 Subject: [PATCH 07/53] Can now create auto budget over API --- app/Api/V1/Controllers/BudgetController.php | 11 +- .../Chart/AvailableBudgetController.php | 2 +- .../Controllers/Search/AccountController.php | 1 + .../Search/TransactionController.php | 13 +-- .../Controllers/Search/TransferController.php | 4 +- app/Api/V1/Controllers/SummaryController.php | 1 - app/Api/V1/Requests/BudgetStoreRequest.php | 106 ++++++++++++++++++ ...getRequest.php => BudgetUpdateRequest.php} | 26 ++--- app/Api/V1/Requests/Request.php | 1 + .../V1/Requests/Search/TransferRequest.php | 1 + app/Http/Requests/BudgetFormStoreRequest.php | 2 +- app/Http/Requests/Request.php | 26 ++++- app/Repositories/Budget/BudgetRepository.php | 31 ++++- app/Transformers/BudgetTransformer.php | 4 +- resources/lang/en_US/validation.php | 1 + 15 files changed, 183 insertions(+), 47 deletions(-) create mode 100644 app/Api/V1/Requests/BudgetStoreRequest.php rename app/Api/V1/Requests/{BudgetRequest.php => BudgetUpdateRequest.php} (71%) diff --git a/app/Api/V1/Controllers/BudgetController.php b/app/Api/V1/Controllers/BudgetController.php index ee41e4efda..22f6efc5b3 100644 --- a/app/Api/V1/Controllers/BudgetController.php +++ b/app/Api/V1/Controllers/BudgetController.php @@ -25,7 +25,8 @@ namespace FireflyIII\Api\V1\Controllers; use Exception; use FireflyIII\Api\V1\Requests\BudgetLimitRequest; -use FireflyIII\Api\V1\Requests\BudgetRequest; +use FireflyIII\Api\V1\Requests\BudgetStoreRequest; +use FireflyIII\Api\V1\Requests\BudgetUpdateRequest; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\Budget; @@ -179,13 +180,13 @@ class BudgetController extends Controller /** * Store a budget. * - * @param BudgetRequest $request + * @param BudgetStoreRequest $request * * @return JsonResponse * @throws FireflyException * */ - public function store(BudgetRequest $request): JsonResponse + public function store(BudgetStoreRequest $request): JsonResponse { $budget = $this->repository->store($request->getAll()); $manager = $this->getManager(); @@ -291,12 +292,12 @@ class BudgetController extends Controller /** * Update a budget. * - * @param BudgetRequest $request + * @param BudgetUpdateRequest $request * @param Budget $budget * * @return JsonResponse */ - public function update(BudgetRequest $request, Budget $budget): JsonResponse + public function update(BudgetUpdateRequest $request, Budget $budget): JsonResponse { $data = $request->getAll(); $budget = $this->repository->update($budget, $data); diff --git a/app/Api/V1/Controllers/Chart/AvailableBudgetController.php b/app/Api/V1/Controllers/Chart/AvailableBudgetController.php index 5181dcc707..62b39fe4ac 100644 --- a/app/Api/V1/Controllers/Chart/AvailableBudgetController.php +++ b/app/Api/V1/Controllers/Chart/AvailableBudgetController.php @@ -85,7 +85,7 @@ class AvailableBudgetController extends Controller } $left = bcadd($availableBudget->amount, (string)$spent); // left less than zero? Set to zero. - if (bccomp($left, '0') === -1) { + if (-1 === bccomp($left, '0')) { $left = '0'; } diff --git a/app/Api/V1/Controllers/Search/AccountController.php b/app/Api/V1/Controllers/Search/AccountController.php index 6fcea80239..72b44ce2ea 100644 --- a/app/Api/V1/Controllers/Search/AccountController.php +++ b/app/Api/V1/Controllers/Search/AccountController.php @@ -1,4 +1,5 @@ . + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V1\Requests; + +use FireflyIII\Rules\IsBoolean; +use Illuminate\Validation\Validator; + +/** + * Class BudgetStoreRequest + * + * @codeCoverageIgnore + */ +class BudgetStoreRequest extends Request +{ + /** + * Authorize logged in users. + * + * @return bool + */ + public function authorize(): bool + { + // Only allow authenticated users + return auth()->check(); + } + + /** + * Get all data from the request. + * + * @return array + */ + public function getAll(): array + { + $active = true; + if (null !== $this->get('active')) { + $active = $this->boolean('active'); + } + + return [ + 'name' => $this->string('name'), + 'active' => $active, + 'order' => 0, + 'auto_budget_type' => $this->string('auto_budget_type'), + 'transaction_currency_id' => $this->integer('auto_budget_currency_id'), + 'transaction_currency_code' => $this->string('auto_budget_currency_code'), + 'auto_budget_amount' => $this->string('auto_budget_amount'), + 'auto_budget_period' => $this->string('auto_budget_period'), + ]; + } + + /** + * The rules that the incoming request must be matched against. + * + * @return array + */ + public function rules(): array + { + return [ + 'name' => 'required|between:1,100|uniqueObjectForUser:budgets,name', + 'active' => [new IsBoolean], + 'auto_budget_type' => 'in:reset,rollover', + 'auto_budget_currency_id' => 'exists:transaction_currencies,id', + 'auto_budget_currency_code' => 'exists:transaction_currencies,code', + 'auto_budget_amount' => 'min:0|max:1000000000', + 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', + ]; + } + + + /** + * Configure the validator instance with special rules for after the basic validation rules. + * + * @param Validator $validator + * + * @return void + */ + public function withValidator(Validator $validator): void + { + $validator->after( + function (Validator $validator) { + // validate all account info + $this->validateAutoBudgetAmount($validator); + } + ); + } +} diff --git a/app/Api/V1/Requests/BudgetRequest.php b/app/Api/V1/Requests/BudgetUpdateRequest.php similarity index 71% rename from app/Api/V1/Requests/BudgetRequest.php rename to app/Api/V1/Requests/BudgetUpdateRequest.php index b33244d599..738c5ff35a 100644 --- a/app/Api/V1/Requests/BudgetRequest.php +++ b/app/Api/V1/Requests/BudgetUpdateRequest.php @@ -1,6 +1,6 @@ 'required|between:1,100|uniqueObjectForUser:budgets,name', + $budget = $this->route()->parameter('budget'); + + return [ + 'name' => sprintf('required|between:1,100|uniqueObjectForUser:budgets,name,%d', $budget->id), 'active' => [new IsBoolean], ]; - switch ($this->method()) { - default: - break; - case 'PUT': - case 'PATCH': - /** @var Budget $budget */ - $budget = $this->route()->parameter('budget'); - $rules['name'] = sprintf('required|between:1,100|uniqueObjectForUser:budgets,name,%d', $budget->id); - break; - } - - return $rules; } } diff --git a/app/Api/V1/Requests/Request.php b/app/Api/V1/Requests/Request.php index 05495c314d..5c3989c7b1 100644 --- a/app/Api/V1/Requests/Request.php +++ b/app/Api/V1/Requests/Request.php @@ -34,4 +34,5 @@ use FireflyIII\Http\Requests\Request as FireflyIIIRequest; */ class Request extends FireflyIIIRequest { + } diff --git a/app/Api/V1/Requests/Search/TransferRequest.php b/app/Api/V1/Requests/Search/TransferRequest.php index 6ee29592f8..52a046170c 100644 --- a/app/Api/V1/Requests/Search/TransferRequest.php +++ b/app/Api/V1/Requests/Search/TransferRequest.php @@ -1,4 +1,5 @@ $this->string('name'), 'active' => $this->boolean('active'), - 'auto_budget_option' => $this->integer('auto_budget_option'), + 'auto_budget_type' => $this->integer('auto_budget_type'), 'transaction_currency_id' => $this->integer('transaction_currency_id'), 'auto_budget_amount' => $this->string('auto_budget_amount'), 'auto_budget_period' => $this->string('auto_budget_period'), diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index f98e2e0b61..efbdad3311 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -387,17 +387,27 @@ class Request extends FormRequest return $data; } + /** * @param Validator $validator */ protected function validateAutoBudgetAmount(Validator $validator): void { - $data = $validator->getData(); - $option = (int)$data['auto_budget_option']; - $amount = $data['auto_budget_amount'] ?? ''; - switch ($option) { + $data = $validator->getData(); + $type = $data['auto_budget_type'] ?? ''; + $amount = $data['auto_budget_amount'] ?? ''; + $period = (string)($data['auto_budget_period'] ?? ''); + $currencyId = $data['auto_budget_currency_id'] ?? ''; + $currencyCode = $data['auto_budget_currency_code'] ?? ''; + if (is_numeric($type)) { + $type = (int)$type; + } + + switch ($type) { case AutoBudget::AUTO_BUDGET_RESET: case AutoBudget::AUTO_BUDGET_ROLLOVER: + case 'reset': + case 'rollover': // basic float check: if ('' === $amount) { $validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget')); @@ -405,8 +415,16 @@ class Request extends FormRequest if (1 !== bccomp((string)$amount, '0')) { $validator->errors()->add('auto_budget_amount', (string)trans('validation.auto_budget_amount_positive')); } + if ('' === $period) { + $validator->errors()->add('auto_budget_period', (string)trans('validation.auto_budget_period_mandatory')); + } + if('' === $currencyCode && '' === $currencyId) { + $validator->errors()->add('auto_budget_amount', (string)trans('validation.require_currency_info')); + } + break; } } + } diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index e4ae35eeec..b83d0da209 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -25,13 +25,14 @@ namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; use DB; use Exception; -use FireflyIII\Models\AutoBudget; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\AutoBudget; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\RecurrenceTransactionMeta; use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleTrigger; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Services\Internal\Destroy\BudgetDestroyService; use FireflyIII\User; use Illuminate\Database\QueryException; @@ -290,17 +291,37 @@ class BudgetRepository implements BudgetRepositoryInterface } // try to create associated auto budget: - $option = $data['auto_budget_option'] ?? 0; - if (0 === $option) { + $type = $data['auto_budget_type'] ?? 0; + if (0 === $type) { return $newBudget; } + if ('reset' === $type) { + $type = AutoBudget::AUTO_BUDGET_RESET; + } + if ('rollover' === $type) { + $type = AutoBudget::AUTO_BUDGET_ROLLOVER; + } + $repos = app(CurrencyRepositoryInterface::class); + $currencyId = (int)($data['transaction_currency_id'] ?? 0); + $currencyCode = (string)($data['transaction_currency_code'] ?? ''); + + + $currency = $repos->findNull($currencyId); + if(null === $currency) { + $currency = $repos->findByCodeNull($currencyCode); + } + if(null === $currency) { + $currency = app('amount')->getDefaultCurrencyByUser($this->user); + } + $autoBudget = new AutoBudget; $autoBudget->budget()->associate($newBudget); - $autoBudget->transaction_currency_id = $data['transaction_currency_id'] ?? 1; - $autoBudget->auto_budget_type = $option; + $autoBudget->transaction_currency_id = $currency->id; + $autoBudget->auto_budget_type = $type; $autoBudget->amount = $data['auto_budget_amount'] ?? '1'; $autoBudget->period = $data['auto_budget_period'] ?? 'monthly'; $autoBudget->save(); + return $newBudget; } diff --git a/app/Transformers/BudgetTransformer.php b/app/Transformers/BudgetTransformer.php index 245683107a..f87630b84a 100644 --- a/app/Transformers/BudgetTransformer.php +++ b/app/Transformers/BudgetTransformer.php @@ -98,11 +98,11 @@ class BudgetTransformer extends AbstractTransformer 'updated_at' => $budget->updated_at->toAtomString(), 'active' => $budget->active, 'name' => $budget->name, + 'auto_budget_type' => $abType, + 'auto_budget_period' => $abPeriod, 'auto_budget_currency_id' => $abCurrencyId, 'auto_budget_currency_code' => $abCurrencyCode, - 'auto_budget_type' => $abType, 'auto_budget_amount' => $abAmount, - 'auto_budget_period' => $abPeriod, 'spent' => $spent, 'links' => [ [ diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index caaed38148..1cfa4b0898 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -204,4 +204,5 @@ return [ 'amount_required_for_auto_budget' => 'The amount is required.', 'auto_budget_amount_positive' => 'The amount must be more than zero.', + 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', ]; From bde0732135bdaface8adf950ae7b6a58952f0ab0 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 07:43:49 +0100 Subject: [PATCH 08/53] Can now update (and remove) auto budget over api --- app/Api/V1/Requests/BudgetStoreRequest.php | 2 +- app/Api/V1/Requests/BudgetUpdateRequest.php | 38 +++++++++++++-- app/Repositories/Budget/BudgetRepository.php | 46 +++++++++++++++++-- .../Budget/BudgetRepositoryInterface.php | 5 ++ 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/app/Api/V1/Requests/BudgetStoreRequest.php b/app/Api/V1/Requests/BudgetStoreRequest.php index 5c3f530f0e..21799684ac 100644 --- a/app/Api/V1/Requests/BudgetStoreRequest.php +++ b/app/Api/V1/Requests/BudgetStoreRequest.php @@ -78,7 +78,7 @@ class BudgetStoreRequest extends Request return [ 'name' => 'required|between:1,100|uniqueObjectForUser:budgets,name', 'active' => [new IsBoolean], - 'auto_budget_type' => 'in:reset,rollover', + 'auto_budget_type' => 'in:reset,rollover,none', 'auto_budget_currency_id' => 'exists:transaction_currencies,id', 'auto_budget_currency_code' => 'exists:transaction_currencies,code', 'auto_budget_amount' => 'min:0|max:1000000000', diff --git a/app/Api/V1/Requests/BudgetUpdateRequest.php b/app/Api/V1/Requests/BudgetUpdateRequest.php index 738c5ff35a..0861c5be25 100644 --- a/app/Api/V1/Requests/BudgetUpdateRequest.php +++ b/app/Api/V1/Requests/BudgetUpdateRequest.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests; use FireflyIII\Rules\IsBoolean; +use Illuminate\Validation\Validator; /** * Class BudgetUpdateRequest @@ -56,9 +57,14 @@ class BudgetUpdateRequest extends Request } return [ - 'name' => $this->string('name'), - 'active' => $active, - 'order' => 0, + 'name' => $this->string('name'), + 'active' => $active, + 'order' => 0, + 'auto_budget_type' => $this->string('auto_budget_type'), + 'transaction_currency_id' => $this->integer('auto_budget_currency_id'), + 'transaction_currency_code' => $this->string('auto_budget_currency_code'), + 'auto_budget_amount' => $this->string('auto_budget_amount'), + 'auto_budget_period' => $this->string('auto_budget_period'), ]; } @@ -72,8 +78,30 @@ class BudgetUpdateRequest extends Request $budget = $this->route()->parameter('budget'); return [ - 'name' => sprintf('required|between:1,100|uniqueObjectForUser:budgets,name,%d', $budget->id), - 'active' => [new IsBoolean], + 'name' => sprintf('required|between:1,100|uniqueObjectForUser:budgets,name,%d', $budget->id), + 'active' => [new IsBoolean], + 'auto_budget_type' => 'in:reset,rollover,none', + 'auto_budget_currency_id' => 'exists:transaction_currencies,id', + 'auto_budget_currency_code' => 'exists:transaction_currencies,code', + 'auto_budget_amount' => 'min:0|max:1000000000', + 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', ]; } + + /** + * Configure the validator instance with special rules for after the basic validation rules. + * + * @param Validator $validator + * + * @return void + */ + public function withValidator(Validator $validator): void + { + $validator->after( + function (Validator $validator) { + // validate all account info + $this->validateAutoBudgetAmount($validator); + } + ); + } } diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index b83d0da209..6820074bfd 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -305,7 +305,6 @@ class BudgetRepository implements BudgetRepositoryInterface $currencyId = (int)($data['transaction_currency_id'] ?? 0); $currencyCode = (string)($data['transaction_currency_code'] ?? ''); - $currency = $repos->findNull($currencyId); if(null === $currency) { $currency = $repos->findByCodeNull($currencyCode); @@ -339,20 +338,48 @@ class BudgetRepository implements BudgetRepositoryInterface $budget->save(); // update or create auto-budget: - $autoBudgetType = $data['auto_budget_option'] ?? 0; + $autoBudgetType = $data['auto_budget_type'] ?? 0; + if ('reset' === $autoBudgetType) { + $autoBudgetType = AutoBudget::AUTO_BUDGET_RESET; + } + if ('rollover' === $autoBudgetType) { + $autoBudgetType = AutoBudget::AUTO_BUDGET_ROLLOVER; + } + if ('none' === $autoBudgetType) { + $autoBudgetType = 0; + } + if (0 !== $autoBudgetType) { $autoBudget = $this->getAutoBudget($budget); if (null === $autoBudget) { $autoBudget = new AutoBudget; $autoBudget->budget()->associate($budget); } - $autoBudget->transaction_currency_id = $data['transaction_currency_id'] ?? 1; + + $repos = app(CurrencyRepositoryInterface::class); + $currencyId = (int)($data['transaction_currency_id'] ?? 0); + $currencyCode = (string)($data['transaction_currency_code'] ?? ''); + + $currency = $repos->findNull($currencyId); + if(null === $currency) { + $currency = $repos->findByCodeNull($currencyCode); + } + if(null === $currency) { + $currency = app('amount')->getDefaultCurrencyByUser($this->user); + } + + $autoBudget->transaction_currency_id = $currency->id; $autoBudget->auto_budget_type = $autoBudgetType; $autoBudget->amount = $data['auto_budget_amount'] ?? '0'; $autoBudget->period = $data['auto_budget_period'] ?? 'monthly'; $autoBudget->save(); } - + if (0 === $autoBudgetType) { + $autoBudget = $this->getAutoBudget($budget); + if (null !== $autoBudget) { + $this->destroyAutoBudget($budget); + } + } $this->updateRuleTriggers($oldName, $data['name']); $this->updateRuleActions($oldName, $data['name']); app('preferences')->mark(); @@ -425,4 +452,15 @@ class BudgetRepository implements BudgetRepositoryInterface { return $budget->autoBudgets()->first(); } + + /** + * @inheritDoc + */ + public function destroyAutoBudget(Budget $budget): void + { + /** @var AutoBudget $autoBudget */ + foreach ($budget->autoBudgets()->get() as $autoBudget) { + $autoBudget->delete(); + } + } } diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index a58d29a96f..c7e008f96f 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -46,6 +46,11 @@ interface BudgetRepositoryInterface */ public function getAutoBudget(Budget $budget): ?AutoBudget; + /** + * @param Budget $budget + */ + public function destroyAutoBudget(Budget $budget): void; + /** * @return bool From fdffed636f9a8112e76871e1ee7bf62bad772393 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 07:49:11 +0100 Subject: [PATCH 09/53] Make sure create uses the right fields. --- app/Http/Controllers/Budget/CreateController.php | 6 +++--- app/Http/Requests/BudgetFormStoreRequest.php | 6 +++--- resources/lang/en_US/firefly.php | 1 + resources/lang/en_US/form.php | 3 ++- resources/views/v1/budgets/create.twig | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Budget/CreateController.php b/app/Http/Controllers/Budget/CreateController.php index d7b79673d8..0cb2a9c201 100644 --- a/app/Http/Controllers/Budget/CreateController.php +++ b/app/Http/Controllers/Budget/CreateController.php @@ -71,8 +71,8 @@ class CreateController extends Controller { $hasOldInput = null !== $request->old('_token'); - // auto budget options - $autoBudgetOptions = [ + // auto budget types + $autoBudgetTypes = [ 0 => (string)trans('firefly.auto_budget_none'), AutoBudget::AUTO_BUDGET_RESET => (string)trans('firefly.auto_budget_reset'), AutoBudget::AUTO_BUDGET_ROLLOVER => (string)trans('firefly.auto_budget_rollover'), @@ -101,7 +101,7 @@ class CreateController extends Controller $request->session()->forget('budgets.create.fromStore'); $subTitle = (string)trans('firefly.create_new_budget'); - return view('budgets.create', compact('subTitle', 'autoBudgetOptions', 'autoBudgetPeriods')); + return view('budgets.create', compact('subTitle', 'autoBudgetTypes', 'autoBudgetPeriods')); } diff --git a/app/Http/Requests/BudgetFormStoreRequest.php b/app/Http/Requests/BudgetFormStoreRequest.php index 83dbd1fed9..330c000632 100644 --- a/app/Http/Requests/BudgetFormStoreRequest.php +++ b/app/Http/Requests/BudgetFormStoreRequest.php @@ -50,7 +50,7 @@ class BudgetFormStoreRequest extends Request return [ 'name' => $this->string('name'), 'active' => $this->boolean('active'), - 'auto_budget_type' => $this->integer('auto_budget_type'), + 'auto_budget_type' => $this->integer('auto_budget_type'), 'transaction_currency_id' => $this->integer('transaction_currency_id'), 'auto_budget_amount' => $this->string('auto_budget_amount'), 'auto_budget_period' => $this->string('auto_budget_period'), @@ -67,8 +67,8 @@ class BudgetFormStoreRequest extends Request return [ 'name' => 'required|between:1,100|uniqueObjectForUser:budgets,name', 'active' => 'numeric|between:0,1', - 'auto_budget_option' => 'numeric|between:0,2', - 'transaction_currency_id' => 'required|exists:transaction_currencies,id', + 'auto_budget_type' => 'numeric|between:0,2', + 'auto_budget_currency_id' => 'required|exists:transaction_currencies,id', 'auto_budget_amount' => 'min:0|max:1000000000', 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', ]; diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 7234877b75..55372eb1dd 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -792,6 +792,7 @@ return [ 'auto_budget_period_quarterly' => 'Quarterly', 'auto_budget_period_half_year' => 'Every half year', 'auto_budget_period_yearly' => 'Yearly', + 'auto_budget_help' => 'You can read more about this feature in the help. Click the top-right (?) icon.', // bills: 'match_between_amounts' => 'Bill matches transactions between :low and :high.', diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index 73952ff96f..e9fed2e35f 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -43,6 +43,7 @@ return [ 'journal_currency_id' => 'Currency', 'currency_id' => 'Currency', 'transaction_currency_id' => 'Currency', + 'auto_budget_currency_id' => 'Currency', 'external_ip' => 'Your server\'s external IP', 'attachments' => 'Attachments', 'journal_amount' => 'Amount', @@ -260,7 +261,7 @@ return [ 'expected_on' => 'Expected on', 'paid' => 'Paid', - 'auto_budget_option' => 'Auto-budget', + 'auto_budget_type' => 'Auto-budget', 'auto_budget_amount' => 'Auto-budget amount', 'auto_budget_period' => 'Auto-budget period', diff --git a/resources/views/v1/budgets/create.twig b/resources/views/v1/budgets/create.twig index 91713d29e8..7af530501a 100644 --- a/resources/views/v1/budgets/create.twig +++ b/resources/views/v1/budgets/create.twig @@ -28,8 +28,8 @@

{{ 'optionalFields'|_ }}

- {{ ExpandedForm.select('auto_budget_option', autoBudgetOptions) }} - {{ CurrencyForm.currencyList('transaction_currency_id') }} + {{ ExpandedForm.select('auto_budget_type', autoBudgetTypes,null, {helpText: trans('firefly.auto_budget_help')}) }} + {{ CurrencyForm.currencyList('auto_budget_currency_id') }} {{ ExpandedForm.amountNoCurrency('auto_budget_amount') }} {{ ExpandedForm.select('auto_budget_period', autoBudgetPeriods, null) }}
From 9bb2f1cfd3aefa5422c71f47f4f0f91dea9ecb9f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 07:55:00 +0100 Subject: [PATCH 10/53] Edit works again --- .../Controllers/Budget/CreateController.php | 2 +- app/Http/Controllers/Budget/EditController.php | 17 ++++++++--------- app/Http/Requests/BudgetFormUpdateRequest.php | 6 +++--- app/Repositories/Budget/BudgetRepository.php | 1 - resources/views/v1/budgets/edit.twig | 4 ++-- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/Budget/CreateController.php b/app/Http/Controllers/Budget/CreateController.php index 0cb2a9c201..b4b203e2a4 100644 --- a/app/Http/Controllers/Budget/CreateController.php +++ b/app/Http/Controllers/Budget/CreateController.php @@ -89,7 +89,7 @@ class CreateController extends Controller $preFilled = [ 'auto_budget_period' => $hasOldInput ? (bool)$request->old('auto_budget_period') : 'monthly', - 'transaction_currency_id' => $hasOldInput ? (int)$request->old('transaction_currency_id') : $currency->id, + 'auto_budget_currency_id' => $hasOldInput ? (int)$request->old('auto_budget_currency_id') : $currency->id, ]; $request->session()->flash('preFilled', $preFilled); diff --git a/app/Http/Controllers/Budget/EditController.php b/app/Http/Controllers/Budget/EditController.php index 536f827c73..b150d5eb9f 100644 --- a/app/Http/Controllers/Budget/EditController.php +++ b/app/Http/Controllers/Budget/EditController.php @@ -24,9 +24,9 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Budget; -use FireflyIII\Models\AutoBudget; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\BudgetFormUpdateRequest; +use FireflyIII\Models\AutoBudget; use FireflyIII\Models\Budget; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use Illuminate\Http\RedirectResponse; @@ -72,8 +72,9 @@ class EditController extends Controller { $subTitle = (string)trans('firefly.edit_budget', ['name' => $budget->name]); $autoBudget = $this->repository->getAutoBudget($budget); - // auto budget options - $autoBudgetOptions = [ + + // auto budget types + $autoBudgetTypes = [ 0 => (string)trans('firefly.auto_budget_none'), AutoBudget::AUTO_BUDGET_RESET => (string)trans('firefly.auto_budget_reset'), AutoBudget::AUTO_BUDGET_ROLLOVER => (string)trans('firefly.auto_budget_rollover'), @@ -89,15 +90,13 @@ class EditController extends Controller // code to handle active-checkboxes $hasOldInput = null !== $request->old('_token'); + $currency = app('amount')->getDefaultCurrency(); $preFilled = [ - 'active' => $hasOldInput ? (bool)$request->old('active') : $budget->active, + 'active' => $hasOldInput ? (bool)$request->old('active') : $budget->active, + 'auto_budget_currency_id' => $hasOldInput ? (int)$request->old('auto_budget_currency_id') : $currency->id, ]; if($autoBudget) { $preFilled['auto_budget_amount'] = $hasOldInput ? $request->old('auto_budget_amount') : $autoBudget->amount; - //'auto_budget_option' => $request->, - //'transaction_currency_id' => 'required|exists:transaction_currencies,id', - //'auto_budget_amount' => $request->old('auto_budget_amount'), - //'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', } // put previous url in session if not redirect from store (not "return_to_edit"). @@ -107,7 +106,7 @@ class EditController extends Controller $request->session()->forget('budgets.edit.fromUpdate'); $request->session()->flash('preFilled', $preFilled); - return view('budgets.edit', compact('budget', 'subTitle', 'autoBudgetOptions', 'autoBudgetPeriods', 'autoBudget')); + return view('budgets.edit', compact('budget', 'subTitle', 'autoBudgetTypes', 'autoBudgetPeriods', 'autoBudget')); } /** diff --git a/app/Http/Requests/BudgetFormUpdateRequest.php b/app/Http/Requests/BudgetFormUpdateRequest.php index d9c54cfb5f..02ce9bebd4 100644 --- a/app/Http/Requests/BudgetFormUpdateRequest.php +++ b/app/Http/Requests/BudgetFormUpdateRequest.php @@ -51,8 +51,8 @@ class BudgetFormUpdateRequest extends Request return [ 'name' => $this->string('name'), 'active' => $this->boolean('active'), - 'auto_budget_option' => $this->integer('auto_budget_option'), - 'transaction_currency_id' => $this->integer('transaction_currency_id'), + 'auto_budget_type' => $this->integer('auto_budget_type'), + 'transaction_currency_id' => $this->integer('auto_budget_currency_id'), 'auto_budget_amount' => $this->string('auto_budget_amount'), 'auto_budget_period' => $this->string('auto_budget_period'), ]; @@ -78,7 +78,7 @@ class BudgetFormUpdateRequest extends Request 'name' => $nameRule, 'active' => 'numeric|between:0,1', 'auto_budget_option' => 'numeric|between:0,2', - 'transaction_currency_id' => 'required|exists:transaction_currencies,id', + 'auto_budget_currency_id' => 'required|exists:transaction_currencies,id', 'auto_budget_amount' => 'min:0|max:1000000000', 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', ]; diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 6820074bfd..ab32906687 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -348,7 +348,6 @@ class BudgetRepository implements BudgetRepositoryInterface if ('none' === $autoBudgetType) { $autoBudgetType = 0; } - if (0 !== $autoBudgetType) { $autoBudget = $this->getAutoBudget($budget); if (null === $autoBudget) { diff --git a/resources/views/v1/budgets/edit.twig b/resources/views/v1/budgets/edit.twig index 4b64626a92..659cf3b419 100644 --- a/resources/views/v1/budgets/edit.twig +++ b/resources/views/v1/budgets/edit.twig @@ -28,8 +28,8 @@

{{ 'optionalFields'|_ }}

- {{ ExpandedForm.select('auto_budget_option', autoBudgetOptions, autoBudget.auto_budget_type) }} - {{ CurrencyForm.currencyList('transaction_currency_id', autoBudget.transaction_currency_id) }} + {{ ExpandedForm.select('auto_budget_type', autoBudgetTypes, autoBudget.auto_budget_type) }} + {{ CurrencyForm.currencyList('auto_budget_currency_id', autoBudget.transaction_currency_id) }} {{ ExpandedForm.amountNoCurrency('auto_budget_amount', preFilled.auto_budget_amount) }} {{ ExpandedForm.select('auto_budget_period', autoBudgetPeriods, autoBudget.period) }}
From 7ea32046af1e25795ac8f5741d267ddc9a142794 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 08:03:43 +0100 Subject: [PATCH 11/53] Add icon. --- .../Controllers/Budget/IndexController.php | 1 + app/Http/Requests/Request.php | 37 ++++++++----------- resources/lang/en_US/firefly.php | 2 + resources/views/v1/budgets/index.twig | 8 ++++ 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app/Http/Controllers/Budget/IndexController.php b/app/Http/Controllers/Budget/IndexController.php index f783877877..32644396bc 100644 --- a/app/Http/Controllers/Budget/IndexController.php +++ b/app/Http/Controllers/Budget/IndexController.php @@ -160,6 +160,7 @@ class IndexController extends Controller $array = $current->toArray(); $array['spent'] = []; $array['budgeted'] = []; + $array['auto_budget'] = $this->repository->getAutoBudget($current); $budgetLimits = $this->blRepository->getBudgetLimits($current, $start, $end); /** @var BudgetLimit $limit */ diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index efbdad3311..b57524fd38 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -25,7 +25,6 @@ namespace FireflyIII\Http\Requests; use Carbon\Carbon; use Carbon\Exceptions\InvalidDateException; use Exception; -use FireflyIII\Models\AutoBudget; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Validator; use Log; @@ -402,27 +401,21 @@ class Request extends FormRequest if (is_numeric($type)) { $type = (int)$type; } - - switch ($type) { - case AutoBudget::AUTO_BUDGET_RESET: - case AutoBudget::AUTO_BUDGET_ROLLOVER: - case 'reset': - case 'rollover': - // basic float check: - if ('' === $amount) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget')); - } - if (1 !== bccomp((string)$amount, '0')) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.auto_budget_amount_positive')); - } - if ('' === $period) { - $validator->errors()->add('auto_budget_period', (string)trans('validation.auto_budget_period_mandatory')); - } - if('' === $currencyCode && '' === $currencyId) { - $validator->errors()->add('auto_budget_amount', (string)trans('validation.require_currency_info')); - } - - break; + if (0 === $type || 'none' === $type) { + return; + } + // basic float check: + if ('' === $amount) { + $validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget')); + } + if (1 !== bccomp((string)$amount, '0')) { + $validator->errors()->add('auto_budget_amount', (string)trans('validation.auto_budget_amount_positive')); + } + if ('' === $period) { + $validator->errors()->add('auto_budget_period', (string)trans('validation.auto_budget_period_mandatory')); + } + if ('' === $currencyCode && '' === $currencyId) { + $validator->errors()->add('auto_budget_amount', (string)trans('validation.require_currency_info')); } } diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 55372eb1dd..04553a8a45 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -793,6 +793,8 @@ return [ 'auto_budget_period_half_year' => 'Every half year', 'auto_budget_period_yearly' => 'Yearly', 'auto_budget_help' => 'You can read more about this feature in the help. Click the top-right (?) icon.', + 'auto_budget_reset_icon' => 'This budget will be set periodically', + 'auto_budget_rollover_icon' => 'The budget amount will increase periodically', // bills: 'match_between_amounts' => 'Bill matches transactions between :low and :high.', diff --git a/resources/views/v1/budgets/index.twig b/resources/views/v1/budgets/index.twig index 549b6b9bf4..12495926c8 100644 --- a/resources/views/v1/budgets/index.twig +++ b/resources/views/v1/budgets/index.twig @@ -234,6 +234,14 @@
+ {% if budget.auto_budget %} + {% if 1 == budget.auto_budget.auto_budget_type %} + + {% endif %} + {% if 2 == budget.auto_budget.auto_budget_type %} + + {% endif %} + {% endif %} {{ budget.name }} From 1dd3018cb295e4bcfdd4c7d7add10791735ab55f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 10:25:12 +0100 Subject: [PATCH 12/53] Basic cron job for budgeting. --- app/Console/Commands/Tools/Cron.php | 88 +++++-- app/Jobs/CreateAutoBudgetLimits.php | 276 +++++++++++++++++++++ app/Support/Cronjobs/AbstractCronjob.php | 38 +++ app/Support/Cronjobs/AutoBudgetCronjob.php | 88 +++++++ app/Support/Cronjobs/RecurringCronjob.php | 32 --- app/Support/Navigation.php | 4 +- 6 files changed, 474 insertions(+), 52 deletions(-) create mode 100644 app/Jobs/CreateAutoBudgetLimits.php create mode 100644 app/Support/Cronjobs/AutoBudgetCronjob.php diff --git a/app/Console/Commands/Tools/Cron.php b/app/Console/Commands/Tools/Cron.php index 0c1fb62af0..9f96d58f03 100644 --- a/app/Console/Commands/Tools/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -28,8 +28,10 @@ use Carbon\Carbon; use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Support\Cronjobs\RecurringCronjob; +use FireflyIII\Support\Cronjobs\AutoBudgetCronjob; use Illuminate\Console\Command; use InvalidArgumentException; +use Log; /** * Class Cron @@ -56,39 +58,38 @@ class Cron extends Command /** * @return int - * @throws Exception */ public function handle(): int { $date = null; try { $date = new Carbon($this->option('date')); - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException|Exception $e) { $this->error(sprintf('"%s" is not a valid date', $this->option('date'))); $e->getMessage(); } + $force = (bool)$this->option('force'); - - $recurring = new RecurringCronjob; - $recurring->setForce($this->option('force')); - - // set date in cron job: - if (null !== $date) { - $recurring->setDate($date); - } - + /* + * Fire recurring transaction cron job. + */ try { - $result = $recurring->fire(); + //$this->recurringCronJob($force, $date); } catch (FireflyException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); $this->error($e->getMessage()); + } - return 0; - } - if (false === $result) { - $this->line('The recurring transaction cron job did not fire.'); - } - if (true === $result) { - $this->line('The recurring transaction cron job fired successfully.'); + /* + * Fire auto-budget cron job: + */ + try { + $this->autoBudgetCronJob($force, $date); + } catch (FireflyException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + $this->error($e->getMessage()); } $this->info('More feedback on the cron jobs can be found in the log files.'); @@ -96,5 +97,54 @@ class Cron extends Command return 0; } + /** + * @param bool $force + * @param Carbon|null $date + * @throws FireflyException + */ + private function autoBudgetCronJob(bool $force, ?Carbon $date) + { + $autoBudget = new AutoBudgetCronjob; + $autoBudget->setForce($force); + // set date in cron job: + if (null !== $date) { + $autoBudget->setDate($date); + } + $result = $autoBudget->fire(); + + if (false === $result) { + $this->line('The auto budget cron job did not fire.'); + } + if (true === $result) { + $this->line('The auto budget cron job fired successfully.'); + } + + } + + /** + * @param bool $force + * @param Carbon|null $date + * + * @throws FireflyException + */ + private function recurringCronJob(bool $force, ?Carbon $date): void + { + $recurring = new RecurringCronjob; + $recurring->setForce($force); + + // set date in cron job: + if (null !== $date) { + $recurring->setDate($date); + } + + $result = $recurring->fire(); + + if (false === $result) { + $this->line('The recurring transaction cron job did not fire.'); + } + if (true === $result) { + $this->line('The recurring transaction cron job fired successfully.'); + } + } } diff --git a/app/Jobs/CreateAutoBudgetLimits.php b/app/Jobs/CreateAutoBudgetLimits.php new file mode 100644 index 0000000000..8fa6e7a5f1 --- /dev/null +++ b/app/Jobs/CreateAutoBudgetLimits.php @@ -0,0 +1,276 @@ +. + */ + +namespace FireflyIII\Jobs; + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\AutoBudget; +use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Collection; +use Log; + +/** + * Class CreateAutoBudgetLimits + */ +class CreateAutoBudgetLimits implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + /** @var Carbon The current date */ + private $date; + + /** + * Create a new job instance. + * + * @codeCoverageIgnore + * + * @param Carbon $date + */ + public function __construct(?Carbon $date) + { + if (null !== $date) { + $date->startOfDay(); + $this->date = $date; + } + Log::debug(sprintf('Created new CreateAutoBudgetLimits("%s")', $this->date->format('Y-m-d'))); + } + + /** + * Execute the job. + * + * @throws FireflyException + */ + public function handle(): void + { + Log::debug(sprintf('Now at start of CreateAutoBudgetLimits() job for %s.', $this->date->format('D d M Y'))); + $autoBudgets = AutoBudget::get(); + Log::debug(sprintf('Found %d auto budgets.', $autoBudgets->count())); + foreach ($autoBudgets as $autoBudget) { + $this->handleAutoBudget($autoBudget); + } + } + + /** + * @param Carbon $date + */ + public function setDate(Carbon $date): void + { + $date->startOfDay(); + $this->date = $date; + } + + /** + * @param AutoBudget $autoBudget + * @param Carbon $start + * @param Carbon $end + * @param string|null $amount + */ + private function createBudgetLimit(AutoBudget $autoBudget, Carbon $start, Carbon $end, ?string $amount = null) + { + Log::debug(sprintf('No budget limit exist. Must create one for auto-budget #%d', $autoBudget->id)); + if (null !== $amount) { + Log::debug(sprintf('Amount is overruled and will be set to %s', $amount)); + } + $budgetLimit = new BudgetLimit; + $budgetLimit->budget()->associate($autoBudget->budget); + $budgetLimit->transactionCurrency()->associate($autoBudget->transactionCurrency); + $budgetLimit->start_date = $start; + $budgetLimit->end_date = $end; + $budgetLimit->amount = $amount ?? $autoBudget->amount; + $budgetLimit->save(); + + Log::debug(sprintf('Created budget limit #%d.', $budgetLimit->id)); + } + + /** + * @param AutoBudget $autoBudget + */ + private function createRollover(AutoBudget $autoBudget): void + { + Log::debug(sprintf('Will now manage rollover for auto budget #%d', $autoBudget->id)); + // current period: + $start = app('navigation')->startOfPeriod($this->date, $autoBudget->period); + $end = app('navigation')->endOfPeriod($start, $autoBudget->period); + + // which means previous period: + $previousStart = app('navigation')->subtractPeriod($start, $autoBudget->period); + $previousEnd = app('navigation')->endOfPeriod($previousStart, $autoBudget->period); + + Log::debug( + sprintf( + 'Current period is %s-%s, so previous period is %s-%s', + $start->format('Y-m-d'), $end->format('Y-m-d'), + $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d') + ) + ); + + // has budget limit in previous period? + $budgetLimit = $this->findBudgetLimit($autoBudget->budget, $previousStart, $previousEnd); + + if (null === $budgetLimit) { + Log::debug('No budget limit exists in previous period, so create one.'); + // if not, create it and we're done. + $this->createBudgetLimit($autoBudget, $start, $end); + Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); + + return; + } + Log::debug('Budget limit exists for previous period.'); + // if has one, calculate expenses and use that as a base. + $repository = app(OperationsRepositoryInterface::class); + $repository->setUser($autoBudget->budget->user); + $spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection([$autoBudget->budget]), $autoBudget->transactionCurrency); + $currencyId = (int)$autoBudget->transaction_currency_id; + $spentAmount = $spent[$currencyId]['sum'] ?? '0'; + Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount)); + + // previous budget limit + this period + spent + $totalAmount = bcadd(bcadd($budgetLimit->amount, $autoBudget->amount), $spentAmount); + Log::debug(sprintf('Total amount for current budget period will be %s', $totalAmount)); + + if (1 !== bccomp($totalAmount, '0')) { + Log::info(sprintf('The total amount is negative, so it will be reset to %s.', $totalAmount)); + $totalAmount = $autoBudget->amount; + } + + // create budget limit: + $this->createBudgetLimit($autoBudget, $start, $end, $totalAmount); + Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); + } + + /** + * @param Budget $budget + * @param Carbon $start + * @param Carbon $end + * + * @return BudgetLimit|null + */ + private function findBudgetLimit(Budget $budget, Carbon $start, Carbon $end): ?BudgetLimit + { + Log::debug( + sprintf( + 'Going to find a budget limit for budget #%d ("%s") between %s and %s', $budget->id, $budget->name, $start->format('Y-m-d'), + $end->format('Y-m-d') + ) + ); + + return $budget->budgetlimits() + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->first(); + } + + /** + * @param AutoBudget $autoBudget + * + * @throws FireflyException + */ + private function handleAutoBudget(AutoBudget $autoBudget): void + { + if (!$this->isMagicDay($autoBudget)) { + Log::info( + sprintf( + 'Today (%s) is not a magic day for %s auto-budget #%d (part of budget #%d "%s")', + $this->date->format('Y-m-d'), $autoBudget->period, $autoBudget->id, $autoBudget->budget->id, $autoBudget->budget->name + ) + ); + Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); + return; + } + Log::info( + sprintf( + 'Today (%s) is a magic day for %s auto-budget #%d (part of budget #%d "%s")', + $this->date->format('Y-m-d'), $autoBudget->period, $autoBudget->id, $autoBudget->budget->id, $autoBudget->budget->name + ) + ); + + // get date range for budget limit, based on range in auto-budget + $start = app('navigation')->startOfPeriod($this->date, $autoBudget->period); + $end = app('navigation')->endOfPeriod($start, $autoBudget->period); + + // find budget limit: + $budgetLimit = $this->findBudgetLimit($autoBudget->budget, $start, $end); + + if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_RESET === $autoBudget->auto_budget_type) { + // that's easy: create one. + // do nothing else. + $this->createBudgetLimit($autoBudget, $start, $end); + Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); + + return; + } + + if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ROLLOVER === $autoBudget->auto_budget_type) { + // budget limit exists already, + $this->createRollover($autoBudget); + Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); + + return; + } + Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); + } + + /** + * @param AutoBudget $autoBudget + * + * @return bool + * @throws FireflyException + */ + private function isMagicDay(AutoBudget $autoBudget): bool + { + switch ($autoBudget->period) { + default: + throw new FireflyException(sprintf('isMagicDay() can\'t handle period "%s"', $autoBudget->period)); + case 'daily': + // every day is magic! + return true; + case 'weekly': + // fire on Monday. + return $this->date->isMonday(); + case 'monthly': + return 1 === $this->date->day; + case 'quarterly': + $format = 'm-d'; + $value = $this->date->format($format); + + return in_array($value, ['01-01', '04-01', '07-01', '10-01'], true); + case 'half_year': + $format = 'm-d'; + $value = $this->date->format($format); + + return in_array($value, ['01-01', '07-01'], true); + break; + case 'yearly': + $format = 'm-d'; + $value = $this->date->format($format); + + return '01-01' === $value; + } + } +} \ No newline at end of file diff --git a/app/Support/Cronjobs/AbstractCronjob.php b/app/Support/Cronjobs/AbstractCronjob.php index d03280f154..5dd9c524c1 100644 --- a/app/Support/Cronjobs/AbstractCronjob.php +++ b/app/Support/Cronjobs/AbstractCronjob.php @@ -23,8 +23,11 @@ declare(strict_types=1); namespace FireflyIII\Support\Cronjobs; +use Carbon\Carbon; +use Exception; /** * Class AbstractCronjob + * * @codeCoverageIgnore */ abstract class AbstractCronjob @@ -32,6 +35,41 @@ abstract class AbstractCronjob /** @var int */ public $timeBetweenRuns = 43200; + /** @var bool */ + protected $force; + + /** @var Carbon */ + protected $date; + + /** + * AbstractCronjob constructor. + * + * @throws Exception + */ + public function __construct() + { + $this->force = false; + $this->date = new Carbon; + } + + + + /** + * @param bool $force + */ + public function setForce(bool $force): void + { + $this->force = $force; + } + + /** + * @param Carbon $date + */ + public function setDate(Carbon $date): void + { + $this->date = $date; + } + /** * @return bool */ diff --git a/app/Support/Cronjobs/AutoBudgetCronjob.php b/app/Support/Cronjobs/AutoBudgetCronjob.php new file mode 100644 index 0000000000..7dba31599c --- /dev/null +++ b/app/Support/Cronjobs/AutoBudgetCronjob.php @@ -0,0 +1,88 @@ +. + */ + +namespace FireflyIII\Support\Cronjobs; + + +use Carbon\Carbon; +use FireflyIII\Jobs\CreateAutoBudgetLimits; +use FireflyIII\Models\Configuration; +use Log; + +/** + * Class AutoBudgetCronjob + */ +class AutoBudgetCronjob extends AbstractCronjob +{ + + /** + * @inheritDoc + */ + public function fire(): bool + { + /** @var Configuration $config */ + $config = app('fireflyconfig')->get('last_ab_job', 0); + $lastTime = (int)$config->data; + $diff = time() - $lastTime; + $diffForHumans = Carbon::now()->diffForHumans(Carbon::createFromTimestamp($lastTime), true); + if (0 === $lastTime) { + Log::info('Auto budget cron-job has never fired before.'); + } + // less than half a day ago: + if ($lastTime > 0 && $diff <= 43200) { + Log::info(sprintf('It has been %s since the auto budget cron-job has fired.', $diffForHumans)); + if (false === $this->force) { + Log::info('The auto budget cron-job will not fire now.'); + + return false; + } + + // fire job regardless. + if (true === $this->force) { + Log::info('Execution of the auto budget cron-job has been FORCED.'); + } + } + + if ($lastTime > 0 && $diff > 43200) { + Log::info(sprintf('It has been %s since the auto budget cron-job has fired. It will fire now!', $diffForHumans)); + } + + $this->fireAutoBudget(); + + app('preferences')->mark(); + + return true; + } + + /** + * + */ + private function fireAutoBudget(): void + { + Log::info(sprintf('Will now fire auto budget cron job task for date "%s".', $this->date->format('Y-m-d'))); + /** @var CreateAutoBudgetLimits $job */ + $job = app(CreateAutoBudgetLimits::class); + $job->setDate($this->date); + $job->handle(); + app('fireflyconfig')->set('last_ab_job', (int)$this->date->format('U')); + Log::info('Done with auto budget cron job task.'); + } +} \ No newline at end of file diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php index 8c851d6a92..dfd7fd413b 100644 --- a/app/Support/Cronjobs/RecurringCronjob.php +++ b/app/Support/Cronjobs/RecurringCronjob.php @@ -34,38 +34,6 @@ use Log; */ class RecurringCronjob extends AbstractCronjob { - /** @var bool */ - private $force; - - /** @var Carbon */ - private $date; - - /** - * RecurringCronjob constructor. - * @throws \Exception - */ - public function __construct() - { - $this->force = false; - $this->date = new Carbon; - } - - /** - * @param bool $force - */ - public function setForce(bool $force): void - { - $this->force = $force; - } - - /** - * @param Carbon $date - */ - public function setDate(Carbon $date): void - { - $this->date = $date; - } - /** * @return bool * @throws FireflyException diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index df0dbe1c50..581e3a4663 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -183,6 +183,7 @@ class Navigation 'quarterly' => 'addMonths', '6M' => 'addMonths', 'half-year' => 'addMonths', + 'half_year' => 'addMonths', 'year' => 'addYear', 'yearly' => 'addYear', '1Y' => 'addYear', @@ -192,10 +193,11 @@ class Navigation '3M' => 3, 'quarterly' => 3, 'half-year' => 6, + 'half_year' => 6, '6M' => 6, ]; - $subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', '1Y', 'year', 'yearly']; + $subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', 'half_year', '1Y', 'year', 'yearly']; // if the range is custom, the end of the period // is another X days (x is the difference between start) From cccaae49a52866b91fbf3039b3cad9394b043349 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 10:27:43 +0100 Subject: [PATCH 13/53] Dont forget to enable recurring cron job. --- app/Console/Commands/Tools/Cron.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/Tools/Cron.php b/app/Console/Commands/Tools/Cron.php index 9f96d58f03..b3e9974ff5 100644 --- a/app/Console/Commands/Tools/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -74,7 +74,7 @@ class Cron extends Command * Fire recurring transaction cron job. */ try { - //$this->recurringCronJob($force, $date); + $this->recurringCronJob($force, $date); } catch (FireflyException $e) { Log::error($e->getMessage()); Log::error($e->getTraceAsString()); From d42b9ee0177d3ab8d38eb0a9f4ad300640933061 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 10:29:23 +0100 Subject: [PATCH 14/53] Initial changelog. --- changelog.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/changelog.md b/changelog.md index 8faa2a6263..dc04b88f56 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). + +## Unreleased [5.2.0 (API 1.1.0)] - 2020-xx-xx + +### Added +- #2578 Budget rollover / auto-set thing. + +### Changed +- Initial release. + +### Deprecated +- Initial release. + +### Removed +- Initial release. + +### Fixed +- Initial release. + +### Security +- Initial release. + +### API +- Initial release + + + ## [5.1.1 (API 1.0.2)] - 2020-03-xx ### Added From c475f05652d7b115b150f3947d39708a1180f584 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 10:29:32 +0100 Subject: [PATCH 15/53] Updated library. --- composer.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index bee1132f85..9767daf171 100644 --- a/composer.lock +++ b/composer.lock @@ -5633,16 +5633,16 @@ }, { "name": "composer/composer", - "version": "1.10.0", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "472c917b2a083ec7d2fa25c55fd099d1300e2515" + "reference": "b912a45da3e2b22f5cb5a23e441b697a295ba011" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/472c917b2a083ec7d2fa25c55fd099d1300e2515", - "reference": "472c917b2a083ec7d2fa25c55fd099d1300e2515", + "url": "https://api.github.com/repos/composer/composer/zipball/b912a45da3e2b22f5cb5a23e441b697a295ba011", + "reference": "b912a45da3e2b22f5cb5a23e441b697a295ba011", "shasum": "" }, "require": { @@ -5709,7 +5709,7 @@ "dependency", "package" ], - "time": "2020-03-10T13:08:05+00:00" + "time": "2020-03-13T19:34:27+00:00" }, { "name": "composer/semver", From a00f46faa9ccb7408741683693e7093b1990e153 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 15:18:32 +0100 Subject: [PATCH 16/53] Also create initial budget limit. --- app/Repositories/Budget/BudgetRepository.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index ab32906687..84a7608399 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -321,6 +321,23 @@ class BudgetRepository implements BudgetRepositoryInterface $autoBudget->period = $data['auto_budget_period'] ?? 'monthly'; $autoBudget->save(); + // create initial budget limit. + $today = new Carbon; + $start = app('navigation')->startOfPeriod($today, $autoBudget->period); + $end = app('navigation')->startOfPeriod($start, $autoBudget->period); + + $limitRepos = app(BudgetLimitRepositoryInterface::class); + $limitRepos->setUser($this->user); + $limitRepos->store( + [ + 'budget_id' => $newBudget->id, + 'transaction_currency_id' => $autoBudget->transaction_currency_id, + 'start_date' => $start->format('Y-m-d'), + 'end_date' => $end->format('Y-m-d'), + 'amount' => $autoBudget->amount, + ] + ); + return $newBudget; } From bfc6a70c9f2e54ccc6d14e6ad84b7dc3e14d9a4b Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 16:56:58 +0100 Subject: [PATCH 17/53] Fix #3154 --- app/Support/Steam.php | 73 +++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 4e6829fffc..da70a85388 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -59,22 +59,24 @@ class Steam } /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); - $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUser($account->user); + $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUser($account->user); // first part: get all balances in own currency: - $nativeBalance = (string)$account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) - ->where('transactions.transaction_currency_id', $currency->id) - ->sum('transactions.amount'); + $transactions = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) + ->where('transactions.transaction_currency_id', $currency->id) + ->get(['transactions.amount'])->toArray(); + $nativeBalance = $this->sumTransactions($transactions, 'amount'); // get all balances in foreign currency: - $foreignBalance = (string)$account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) - ->where('transactions.foreign_currency_id', $currency->id) - ->where('transactions.transaction_currency_id', '!=', $currency->id) - ->sum('transactions.foreign_amount'); + $transactions = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) + ->where('transactions.foreign_currency_id', $currency->id) + ->where('transactions.transaction_currency_id', '!=', $currency->id) + ->get(['transactions.foreign_amount'])->toArray(); + $foreignBalance = $this->sumTransactions($transactions, 'foreign_amount'); // check: Log::debug(sprintf('Steam::balance. Native balance is "%s"', $nativeBalance)); @@ -86,6 +88,7 @@ class Steam Log::debug(sprintf('Steam::balance. Virtual balance is "%s"', $virtual)); $balance = bcadd($balance, $virtual); + $cache->store($balance); return $balance; @@ -114,20 +117,25 @@ class Steam $repository = app(AccountRepositoryInterface::class); $repository->setUser($account->user); - $currencyId = (int)$repository->getMetaValue($account, 'currency_id'); - $nativeBalance = (string)$account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) - ->where('transactions.transaction_currency_id', $currencyId) - ->sum('transactions.amount'); + $currencyId = (int)$repository->getMetaValue($account, 'currency_id'); + + + $transactions = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) + ->where('transactions.transaction_currency_id', $currencyId) + ->get(['transactions.amount'])->toArray(); + $nativeBalance = $this->sumTransactions($transactions, 'amount'); // get all balances in foreign currency: - $foreignBalance = (string)$account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) - ->where('transactions.foreign_currency_id', $currencyId) - ->where('transactions.transaction_currency_id', '!=', $currencyId) - ->sum('transactions.foreign_amount'); + $transactions = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) + ->where('transactions.foreign_currency_id', $currencyId) + ->where('transactions.transaction_currency_id', '!=', $currencyId) + ->get(['transactions.foreign_amount'])->toArray(); + + $foreignBalance = $this->sumTransactions($transactions, 'foreign_amount'); $balance = bcadd($nativeBalance, $foreignBalance); $cache->store($balance); @@ -135,6 +143,23 @@ class Steam return $balance; } + /** + * @param array $transactions + * @param string $key + * + * @return string + */ + public function sumTransactions(array $transactions, string $key): string + { + $sum = '0'; + /** @var array $transaction */ + foreach ($transactions as $transaction) { + $sum = bcadd($sum, $transaction[$key] ?? '0'); + } + + return $sum; + } + /** * Gets the balance for the given account during the whole range, using this format:. * From d1325ffbd80fae71c754cb3fd7026ee005dc0bf1 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 19:12:32 +0100 Subject: [PATCH 18/53] New charts for #2726 --- .../Chart/Basic/GeneratorInterface.php | 4 +- .../Chart/TransactionController.php | 240 ++++++++++++++++++ public/v1/js/ff/transactions/index.js | 28 ++ resources/lang/en_US/firefly.php | 1 + resources/views/v1/transactions/index.twig | 59 +++++ routes/web.php | 15 ++ 6 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/Chart/TransactionController.php create mode 100644 public/v1/js/ff/transactions/index.js diff --git a/app/Generator/Chart/Basic/GeneratorInterface.php b/app/Generator/Chart/Basic/GeneratorInterface.php index 3bf8331f19..b92994f858 100644 --- a/app/Generator/Chart/Basic/GeneratorInterface.php +++ b/app/Generator/Chart/Basic/GeneratorInterface.php @@ -46,7 +46,7 @@ interface GeneratorInterface * 'fill' => if to fill a line? optional, will not be included when unused. * 'entries' => * [ - * 'label-of-entry' => 'value' + * key => [value => x, 'currency_symbol' => 'x'] * ] * ] * 1: [ @@ -56,7 +56,7 @@ interface GeneratorInterface * 'fill' => if to fill a line? optional, will not be included when unused. * 'entries' => * [ - * 'label-of-entry' => 'value' + * key => [value => x, 'currency_symbol' => 'x'] * ] * ] * diff --git a/app/Http/Controllers/Chart/TransactionController.php b/app/Http/Controllers/Chart/TransactionController.php new file mode 100644 index 0000000000..6bea98b9dc --- /dev/null +++ b/app/Http/Controllers/Chart/TransactionController.php @@ -0,0 +1,240 @@ +. + */ + +namespace FireflyIII\Http\Controllers\Chart; + + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Generator\Chart\Basic\GeneratorInterface; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Models\TransactionType; +use FireflyIII\Support\CacheProperties; + +/** + * Class TransactionController + */ +class TransactionController extends Controller +{ + + /** @var GeneratorInterface Chart generation methods. */ + protected $generator; + + /** + * TransactionController constructor. + */ + public function __construct() + { + parent::__construct(); + $this->generator = app(GeneratorInterface::class); + } + + /** + * @param string $objectType + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + * @throws FireflyException + */ + public function budgets(Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('chart.transactions.budgets'); + if ($cache->has()) { + return response()->json($cache->get()); // @codeCoverageIgnore + } + + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end); + $collector->withBudgetInformation(); + $collector->setTypes([TransactionType::WITHDRAWAL]); + + $result = $collector->getExtractedJournals(); + $data = []; + + // group by category. + /** @var array $journal */ + foreach ($result as $journal) { + $budget = $journal['budget_name'] ?? (string)trans('firefly.no_budget'); + $title = sprintf('%s (%s)', $budget, $journal['currency_symbol']); + // key => [value => x, 'currency_symbol' => 'x'] + $data[$title] = $data[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $journal['currency_symbol'], + ]; + $data[$title]['amount'] = bcadd($data[$title]['amount'], $journal['amount']); + + if (null !== $journal['foreign_amount']) { + $title = sprintf('%s (%s)', $budget, $journal['foreign_currency_symbol']); + $data[$title] = $data[$title] ?? [ + 'amount' => $journal['foreign_amount'], + 'currency_symbol' => $journal['currency_symbol'], + ]; + $data[$title]['amount'] = bcadd($data[$title]['amount'], $journal['foreign_amount']); + } + } + $chart = $this->generator->multiCurrencyPieChart($data); + $cache->store($chart); + + return response()->json($chart); + } + + /** + * @param string $objectType + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + * @throws FireflyException + */ + public function categories(string $objectType, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($objectType); + $cache->addProperty('chart.transactions.categories'); + if ($cache->has()) { + return response()->json($cache->get()); // @codeCoverageIgnore + } + + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end); + $collector->withCategoryInformation(); + switch ($objectType) { + default: + throw new FireflyException(sprintf('Cant handle "%s"', $objectType)); + case 'withdrawal': + $collector->setTypes([TransactionType::WITHDRAWAL]); + break; + case 'deposit': + $collector->setTypes([TransactionType::DEPOSIT]); + break; + case 'transfers': + $collector->setTypes([TransactionType::TRANSFER]); + break; + } + $result = $collector->getExtractedJournals(); + $data = []; + + // group by category. + /** @var array $journal */ + foreach ($result as $journal) { + $category = $journal['category_name'] ?? (string)trans('firefly.no_category'); + $title = sprintf('%s (%s)', $category, $journal['currency_symbol']); + // key => [value => x, 'currency_symbol' => 'x'] + $data[$title] = $data[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $journal['currency_symbol'], + ]; + $data[$title]['amount'] = bcadd($data[$title]['amount'], $journal['amount']); + + if (null !== $journal['foreign_amount']) { + $title = sprintf('%s (%s)', $category, $journal['foreign_currency_symbol']); + $data[$title] = $data[$title] ?? [ + 'amount' => $journal['foreign_amount'], + 'currency_symbol' => $journal['currency_symbol'], + ]; + $data[$title]['amount'] = bcadd($data[$title]['amount'], $journal['foreign_amount']); + } + } + $chart = $this->generator->multiCurrencyPieChart($data); + $cache->store($chart); + + return response()->json($chart); + } + + + /** + * @param string $objectType + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + * @throws FireflyException + */ + public function destinationAccounts(string $objectType, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($objectType); + $cache->addProperty('chart.transactions.destinations'); + if ($cache->has()) { + //return response()->json($cache->get()); // @codeCoverageIgnore + } + + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end); + $collector->withAccountInformation(); + switch ($objectType) { + default: + throw new FireflyException(sprintf('Cant handle "%s"', $objectType)); + case 'withdrawal': + $collector->setTypes([TransactionType::WITHDRAWAL]); + break; + case 'deposit': + $collector->setTypes([TransactionType::DEPOSIT]); + break; + case 'transfers': + $collector->setTypes([TransactionType::TRANSFER]); + break; + } + $result = $collector->getExtractedJournals(); + $data = []; + + // group by category. + /** @var array $journal */ + foreach ($result as $journal) { + $name = $journal['destination_account_name']; + $title = sprintf('%s (%s)', $name, $journal['currency_symbol']); + $data[$title] = $data[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $journal['currency_symbol'], + ]; + $data[$title]['amount'] = bcadd($data[$title]['amount'], $journal['amount']); + + if (null !== $journal['foreign_amount']) { + $title = sprintf('%s (%s)', $name, $journal['foreign_currency_symbol']); + $data[$title] = $data[$title] ?? [ + 'amount' => $journal['foreign_amount'], + 'currency_symbol' => $journal['currency_symbol'], + ]; + $data[$title]['amount'] = bcadd($data[$title]['amount'], $journal['foreign_amount']); + } + } + $chart = $this->generator->multiCurrencyPieChart($data); + $cache->store($chart); + + return response()->json($chart); + } + +} \ No newline at end of file diff --git a/public/v1/js/ff/transactions/index.js b/public/v1/js/ff/transactions/index.js new file mode 100644 index 0000000000..93749d5eb9 --- /dev/null +++ b/public/v1/js/ff/transactions/index.js @@ -0,0 +1,28 @@ +/* + * show.js + * Copyright (c) 2019 james@firefly-iii.org + * + * This file is part of Firefly III (https://github.com/firefly-iii). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +$(function () { + "use strict"; + if (!showAll) { + multiCurrencyPieChart(categoryChartUri, 'category_chart'); + multiCurrencyPieChart(budgetChartUri, 'budget_chart'); + multiCurrencyPieChart(destinationChartUri, 'destination_chart'); + } +}); \ No newline at end of file diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 04553a8a45..97bfafa8ef 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -218,6 +218,7 @@ return [ 'unpaid_in_currency' => 'Unpaid in :currency', 'is_alpha_warning' => 'You are running an ALPHA version. Be wary of bugs and issues.', 'is_beta_warning' => 'You are running an BETA version. Be wary of bugs and issues.', + 'all_destination_accounts' => 'Destination accounts', // check for updates: 'update_check_title' => 'Check for updates', diff --git a/resources/views/v1/transactions/index.twig b/resources/views/v1/transactions/index.twig index 5a45763663..17b1db6d3e 100644 --- a/resources/views/v1/transactions/index.twig +++ b/resources/views/v1/transactions/index.twig @@ -15,6 +15,51 @@
{% endif %} + {% if periods|length > 0 %} + + {% set boxSize = 'col-lg-6 col-md-6 col-sm-12 col-xs-12' %} + {% if objectType == 'withdrawal' %} + {% set boxSize = 'col-lg-4 col-md-6 col-sm-12 col-xs-12' %} + {% endif %} +
+ {# for withdrawals, deposits and transfers #} +
+
+
+

{{ 'categories'|_ }}

+
+
+ +
+
+
+ {# only for withdrawals #} + {% if objectType == 'withdrawal' %} +
+
+
+

{{ 'budgets'|_ }}

+
+
+ +
+
+
+ {% endif %} + {# for all #} +
+
+
+

{{ 'all_destination_accounts'|_ }}

+
+
+ +
+
+
+
+ {% endif %} + {# list with journals #}
@@ -68,4 +113,18 @@ {% block scripts %} {# required for groups.twig #} + + + + + + + {% endblock %} diff --git a/routes/web.php b/routes/web.php index e2b95b0769..7734d0abc9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -488,6 +488,21 @@ Route::group( } ); + +/** + * Chart\Transactions Controller + */ +Route::group( + ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Chart', 'prefix' => 'chart/transactions', 'as' => 'chart.transactions.'], + static function () { + Route::get('categories/{objectType}/{start_date}/{end_date}', ['uses' => 'TransactionController@categories', 'as' => 'categories']); + Route::get('budgets/{start_date}/{end_date}', ['uses' => 'TransactionController@budgets', 'as' => 'budgets']); + Route::get('destinationAccounts/{objectType}/{start_date}/{end_date}', ['uses' => 'TransactionController@destinationAccounts', 'as' => 'destinationAccounts']); + // + + } +); + /** * Export controller */ From 50b710b4f6fb7ff0b7265503386acbfa21ff5f80 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 20:30:31 +0100 Subject: [PATCH 19/53] Expand charts. --- .../Chart/TransactionController.php | 67 ++++++++++++++++++- public/v1/js/ff/transactions/index.js | 1 + resources/lang/en_US/firefly.php | 1 + resources/views/v1/transactions/index.twig | 18 +++-- routes/web.php | 1 + 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Chart/TransactionController.php b/app/Http/Controllers/Chart/TransactionController.php index 6bea98b9dc..95b3743eb7 100644 --- a/app/Http/Controllers/Chart/TransactionController.php +++ b/app/Http/Controllers/Chart/TransactionController.php @@ -170,7 +170,6 @@ class TransactionController extends Controller return response()->json($chart); } - /** * @param string $objectType * @param Carbon $start @@ -237,4 +236,70 @@ class TransactionController extends Controller return response()->json($chart); } + /** + * @param string $objectType + * @param Carbon $start + * @param Carbon $end + * + * @return \Illuminate\Http\JsonResponse + * @throws FireflyException + */ + public function sourceAccounts(string $objectType, Carbon $start, Carbon $end) + { + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($objectType); + $cache->addProperty('chart.transactions.sources'); + if ($cache->has()) { + //return response()->json($cache->get()); // @codeCoverageIgnore + } + + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setRange($start, $end); + $collector->withAccountInformation(); + switch ($objectType) { + default: + throw new FireflyException(sprintf('Cant handle "%s"', $objectType)); + case 'withdrawal': + $collector->setTypes([TransactionType::WITHDRAWAL]); + break; + case 'deposit': + $collector->setTypes([TransactionType::DEPOSIT]); + break; + case 'transfers': + $collector->setTypes([TransactionType::TRANSFER]); + break; + } + $result = $collector->getExtractedJournals(); + $data = []; + + // group by category. + /** @var array $journal */ + foreach ($result as $journal) { + $name = $journal['source_account_name']; + $title = sprintf('%s (%s)', $name, $journal['currency_symbol']); + $data[$title] = $data[$title] ?? [ + 'amount' => '0', + 'currency_symbol' => $journal['currency_symbol'], + ]; + $data[$title]['amount'] = bcadd($data[$title]['amount'], $journal['amount']); + + if (null !== $journal['foreign_amount']) { + $title = sprintf('%s (%s)', $name, $journal['foreign_currency_symbol']); + $data[$title] = $data[$title] ?? [ + 'amount' => $journal['foreign_amount'], + 'currency_symbol' => $journal['currency_symbol'], + ]; + $data[$title]['amount'] = bcadd($data[$title]['amount'], $journal['foreign_amount']); + } + } + $chart = $this->generator->multiCurrencyPieChart($data); + $cache->store($chart); + + return response()->json($chart); + } + } \ No newline at end of file diff --git a/public/v1/js/ff/transactions/index.js b/public/v1/js/ff/transactions/index.js index 93749d5eb9..1ce056df8d 100644 --- a/public/v1/js/ff/transactions/index.js +++ b/public/v1/js/ff/transactions/index.js @@ -24,5 +24,6 @@ $(function () { multiCurrencyPieChart(categoryChartUri, 'category_chart'); multiCurrencyPieChart(budgetChartUri, 'budget_chart'); multiCurrencyPieChart(destinationChartUri, 'destination_chart'); + multiCurrencyPieChart(sourceChartUri, 'source_chart'); } }); \ No newline at end of file diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 97bfafa8ef..2d589d5bef 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -219,6 +219,7 @@ return [ 'is_alpha_warning' => 'You are running an ALPHA version. Be wary of bugs and issues.', 'is_beta_warning' => 'You are running an BETA version. Be wary of bugs and issues.', 'all_destination_accounts' => 'Destination accounts', + 'all_source_accounts' => 'Source accounts', // check for updates: 'update_check_title' => 'Check for updates', diff --git a/resources/views/v1/transactions/index.twig b/resources/views/v1/transactions/index.twig index 17b1db6d3e..f50ffd01a4 100644 --- a/resources/views/v1/transactions/index.twig +++ b/resources/views/v1/transactions/index.twig @@ -17,10 +17,7 @@ {% if periods|length > 0 %} - {% set boxSize = 'col-lg-6 col-md-6 col-sm-12 col-xs-12' %} - {% if objectType == 'withdrawal' %} - {% set boxSize = 'col-lg-4 col-md-6 col-sm-12 col-xs-12' %} - {% endif %} + {% set boxSize = 'col-lg-4 col-md-6 col-sm-12 col-xs-12' %}
{# for withdrawals, deposits and transfers #}
@@ -46,6 +43,18 @@
{% endif %} + {% if objectType != 'withdrawal' %} +
+
+
+

{{ 'all_source_accounts'|_ }}

+
+
+ +
+
+
+ {% endif %} {# for all #}
@@ -119,6 +128,7 @@ var categoryChartUri = '{{ route('chart.transactions.categories', [objectType, start.format('Y-m-d'), end.format('Y-m-d')]) }}'; var budgetChartUri = '{{ route('chart.transactions.budgets', [start.format('Y-m-d'), end.format('Y-m-d')]) }}'; var destinationChartUri = '{{ route('chart.transactions.destinationAccounts', [objectType, start.format('Y-m-d'), end.format('Y-m-d')]) }}'; + var sourceChartUri = '{{ route('chart.transactions.sourceAccounts', [objectType, start.format('Y-m-d'), end.format('Y-m-d')]) }}'; diff --git a/routes/web.php b/routes/web.php index 7734d0abc9..1d1542765a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -498,6 +498,7 @@ Route::group( Route::get('categories/{objectType}/{start_date}/{end_date}', ['uses' => 'TransactionController@categories', 'as' => 'categories']); Route::get('budgets/{start_date}/{end_date}', ['uses' => 'TransactionController@budgets', 'as' => 'budgets']); Route::get('destinationAccounts/{objectType}/{start_date}/{end_date}', ['uses' => 'TransactionController@destinationAccounts', 'as' => 'destinationAccounts']); + Route::get('sourceAccounts/{objectType}/{start_date}/{end_date}', ['uses' => 'TransactionController@sourceAccounts', 'as' => 'sourceAccounts']); // } From 33c73701d87580cefa69e2e0ac04bac5773c7365 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 14 Mar 2020 21:01:21 +0100 Subject: [PATCH 20/53] Button to duplicate rule. #2957 --- .../Controllers/Rule/CreateController.php | 16 +++++++++++ app/Repositories/Rule/RuleRepository.php | 28 +++++++++++++++++++ .../Rule/RuleRepositoryInterface.php | 7 +++++ resources/lang/en_US/firefly.php | 3 ++ resources/views/v1/rules/index.twig | 5 ++++ routes/web.php | 1 + 6 files changed, 60 insertions(+) diff --git a/app/Http/Controllers/Rule/CreateController.php b/app/Http/Controllers/Rule/CreateController.php index b80af3a389..561d41a3cf 100644 --- a/app/Http/Controllers/Rule/CreateController.php +++ b/app/Http/Controllers/Rule/CreateController.php @@ -27,6 +27,7 @@ namespace FireflyIII\Http\Controllers\Rule; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\RuleFormRequest; use FireflyIII\Models\Bill; +use FireflyIII\Models\Rule; use FireflyIII\Models\RuleGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; @@ -113,6 +114,21 @@ class CreateController extends Controller ); } + /** + * @param Rule $rule + * + * @return RedirectResponse + */ + public function duplicate(Rule $rule): RedirectResponse + { + /** @var Rule $newRule */ + $newRule = $this->ruleRepos->duplicate($rule); + + session()->flash('success', trans('firefly.duplicated_rule', ['title' => $rule->title,'newTitle' => $newRule->title])); + + return redirect(route('rules.index')); + } + /** * Create a new rule. It will be stored under the given $ruleGroup. * diff --git a/app/Repositories/Rule/RuleRepository.php b/app/Repositories/Rule/RuleRepository.php index 75c5a32e1f..c9b16dbe8a 100644 --- a/app/Repositories/Rule/RuleRepository.php +++ b/app/Repositories/Rule/RuleRepository.php @@ -475,4 +475,32 @@ class RuleRepository implements RuleRepositoryInterface return true; } + + /** + * @inheritDoc + */ + public function duplicate(Rule $rule): Rule + { + $newRule = $rule->replicate(); + $newRule->title = (string)trans('firefly.rule_copy_of', ['title' => $rule->title]); + $newRule->save(); + + // replicate all triggers + /** @var RuleTrigger $trigger */ + foreach ($rule->ruleTriggers as $trigger) { + $newTrigger = $trigger->replicate(); + $newTrigger->rule_id = $newRule->id; + $newTrigger->save(); + } + + // replicate all actions + /** @var RuleAction $action */ + foreach ($rule->ruleActions as $action) { + $newAction = $action->replicate(); + $newAction->rule_id = $newRule->id; + $newAction->save(); + } + + return $newRule; + } } diff --git a/app/Repositories/Rule/RuleRepositoryInterface.php b/app/Repositories/Rule/RuleRepositoryInterface.php index 1cec41bdf7..a979201e34 100644 --- a/app/Repositories/Rule/RuleRepositoryInterface.php +++ b/app/Repositories/Rule/RuleRepositoryInterface.php @@ -46,6 +46,13 @@ interface RuleRepositoryInterface */ public function destroy(Rule $rule): bool; + /** + * @param Rule $rule + * + * @return Rule + */ + public function duplicate(Rule $rule): Rule; + /** * @param int $ruleId * diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 2d589d5bef..c93febc83b 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -307,6 +307,9 @@ return [ 'created_new_rule_group' => 'New rule group ":title" stored!', 'updated_rule_group' => 'Successfully updated rule group ":title".', 'edit_rule_group' => 'Edit rule group ":title"', + 'duplicate_rule' => 'Duplicate rule ":title"', + 'rule_copy_of' => 'Copy of ":title"', + 'duplicated_rule' => 'Duplicated rule ":title" into ":newTitle"', 'delete_rule_group' => 'Delete rule group ":title"', 'deleted_rule_group' => 'Deleted rule group ":title"', 'update_rule_group' => 'Update rule group', diff --git a/resources/views/v1/rules/index.twig b/resources/views/v1/rules/index.twig index ba52eaa5a6..059b00707b 100644 --- a/resources/views/v1/rules/index.twig +++ b/resources/views/v1/rules/index.twig @@ -115,6 +115,11 @@ + + {# duplicate rule #} + +
diff --git a/routes/web.php b/routes/web.php index 1d1542765a..b8d6f0dcf0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -881,6 +881,7 @@ Route::group( Route::get('create-from-bill/{bill}', ['uses' => 'Rule\CreateController@createFromBill', 'as' => 'create-from-bill']); Route::get('create-from-journal/{tj}', ['uses' => 'Rule\CreateController@createFromJournal', 'as' => 'create-from-journal']); Route::post('store', ['uses' => 'Rule\CreateController@store', 'as' => 'store']); + Route::get('duplicate/{rule}', ['uses' => 'Rule\CreateController@duplicate', 'as' => 'duplicate']); // delete controller Route::get('delete/{rule}', ['uses' => 'Rule\DeleteController@delete', 'as' => 'delete']); From f63e51fea2e099fdd3d5ab9b546d520215e01d9a Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 15 Mar 2020 07:48:02 +0100 Subject: [PATCH 21/53] Fix issue with null pointers. --- .../Controllers/Report/BudgetController.php | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/Report/BudgetController.php b/app/Http/Controllers/Report/BudgetController.php index 7ed503d63a..b7eb005984 100644 --- a/app/Http/Controllers/Report/BudgetController.php +++ b/app/Http/Controllers/Report/BudgetController.php @@ -390,6 +390,14 @@ class BudgetController extends Controller ]; $noBudget = $this->nbRepository->sumExpenses($start, $end); foreach ($noBudget as $noBudgetEntry) { + + // currency information: + $nbCurrencyId = (int)($noBudgetEntry['currency_id'] ?? $defaultCurrency->id); + $nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $defaultCurrency->code; + $nbCurrencyName = $noBudgetEntry['currency_name'] ?? $defaultCurrency->name; + $nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $defaultCurrency->symbol; + $nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $defaultCurrency->decimal_places; + $report['budgets'][0]['budget_limits'][] = [ 'budget_limit_id' => null, 'start_date' => $start, @@ -400,25 +408,24 @@ class BudgetController extends Controller 'spent_pct' => '0', 'left' => '0', 'overspent' => '0', - 'currency_id' => (int)($noBudgetEntry['currency_id'] ?? $defaultCurrency->id), - 'currency_code' => $noBudgetEntry['currency_code'] ?? $defaultCurrency->code, - 'currency_name' => $noBudgetEntry['currency_name'] ?? $defaultCurrency->name, - 'currency_symbol' => $noBudgetEntry['currency_symbol'] ?? $defaultCurrency->symbol, - 'currency_decimal_places' => $noBudgetEntry['currency_decimal_places'] ?? $defaultCurrency->decimal_places, + 'currency_id' => $nbCurrencyId, + 'currency_code' => $nbCurrencyCode, + 'currency_name' => $nbCurrencyName, + 'currency_symbol' => $nbCurrencySymbol, + 'currency_decimal_places' => $nbCurrencyDp, ]; - $report['sums'][$noBudgetEntry['currency_id']]['spent'] - = bcadd($report['sums'][$noBudgetEntry['currency_id']]['spent'] ?? '0', $noBudgetEntry['sum']); + $report['sums'][$nbCurrencyId]['spent'] = bcadd($report['sums'][$nbCurrencyId]['spent'] ?? '0', $noBudgetEntry['sum']); // append currency info because it may be missing: - $report['sums'][$noBudgetEntry['currency_id']]['currency_id'] = (int)($noBudgetEntry['currency_id'] ?? $defaultCurrency->id); - $report['sums'][$noBudgetEntry['currency_id']]['currency_code'] = $noBudgetEntry['currency_code'] ?? $defaultCurrency->code; - $report['sums'][$noBudgetEntry['currency_id']]['currency_name'] = $noBudgetEntry['currency_name'] ?? $defaultCurrency->name; - $report['sums'][$noBudgetEntry['currency_id']]['currency_symbol'] = $noBudgetEntry['currency_symbol'] ?? $defaultCurrency->symbol; - $report['sums'][$noBudgetEntry['currency_id']]['currency_decimal_places'] = $noBudgetEntry['currency_decimal_places'] ?? $defaultCurrency->decimal_places; + $report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId; + $report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode; + $report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName; + $report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol; + $report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp; // append other sums because they might be missing: - $report['sums'][$noBudgetEntry['currency_id']]['overspent'] = $report['sums'][$noBudgetEntry['currency_id']]['overspent'] ?? '0'; - $report['sums'][$noBudgetEntry['currency_id']]['left'] = $report['sums'][$noBudgetEntry['currency_id']]['left'] ?? '0'; - $report['sums'][$noBudgetEntry['currency_id']]['budgeted'] = $report['sums'][$noBudgetEntry['currency_id']]['budgeted'] ?? '0'; + $report['sums'][$nbCurrencyId]['overspent'] = $report['sums'][$nbCurrencyId]['overspent'] ?? '0'; + $report['sums'][$nbCurrencyId]['left'] = $report['sums'][$nbCurrencyId]['left'] ?? '0'; + $report['sums'][$nbCurrencyId]['budgeted'] = $report['sums'][$nbCurrencyId]['budgeted'] ?? '0'; } // make percentages based on total amount. @@ -445,7 +452,6 @@ class BudgetController extends Controller $report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct; } } - //var_dump($report);exit; return view('reports.partials.budgets', compact('report'))->render(); } From 6967bb003e6c921d8e230379818ecac963d4947f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 15 Mar 2020 08:16:16 +0100 Subject: [PATCH 22/53] Do some code cleanup. --- app/Api/V1/Controllers/AccountController.php | 3 - .../Controllers/ConfigurationController.php | 6 +- .../Controllers/Search/AccountController.php | 4 +- .../Search/TransactionController.php | 8 +-- .../Controllers/Search/TransferController.php | 5 +- app/Api/V1/Controllers/SummaryController.php | 34 +++--------- .../V1/Controllers/TransactionController.php | 7 +-- .../V1/Requests/AvailableBudgetRequest.php | 4 +- app/Api/V1/Requests/BillRequest.php | 4 +- .../V1/Requests/RecurrenceStoreRequest.php | 5 +- .../V1/Requests/RecurrenceUpdateRequest.php | 5 +- app/Api/V1/Requests/RuleGroupTestRequest.php | 5 +- .../V1/Requests/RuleGroupTriggerRequest.php | 5 +- app/Api/V1/Requests/RuleStoreRequest.php | 9 +-- app/Api/V1/Requests/RuleTestRequest.php | 5 +- app/Api/V1/Requests/RuleTriggerRequest.php | 5 +- app/Api/V1/Requests/RuleUpdateRequest.php | 9 +-- .../V1/Requests/Search/TransferRequest.php | 4 +- .../V1/Requests/TransactionStoreRequest.php | 10 ++-- .../V1/Requests/TransactionUpdateRequest.php | 4 +- app/Api/V1/Requests/UserStoreRequest.php | 5 +- app/Api/V1/Requests/UserUpdateRequest.php | 10 ++-- .../CorrectOpeningBalanceCurrencies.php | 7 +-- .../Commands/Correction/DeleteEmptyGroups.php | 3 +- .../Correction/DeleteOrphanedTransactions.php | 8 ++- .../Commands/Correction/FixAccountTypes.php | 20 ++++--- .../Correction/FixRecurringTransactions.php | 10 ---- .../Commands/Correction/FixUnevenAmount.php | 6 +- app/Console/Commands/DecryptDatabase.php | 2 +- app/Console/Commands/Export/ExportData.php | 12 +--- .../Commands/Import/CreateCSVImport.php | 4 +- .../Commands/Integrity/RestoreOAuthKeys.php | 4 +- app/Console/Commands/ScanAttachments.php | 1 - app/Console/Commands/SetLatestVersion.php | 10 ---- app/Console/Commands/Tools/Cron.php | 4 +- .../Commands/Upgrade/AccountCurrencies.php | 3 +- .../Upgrade/MigrateRecurrenceMeta.php | 2 +- .../Commands/Upgrade/MigrateToGroups.php | 20 +++++-- .../Commands/Upgrade/MigrateToRules.php | 6 +- .../Upgrade/OtherCurrenciesCorrections.php | 11 ++-- .../Commands/Upgrade/RenameAccountMeta.php | 2 +- .../Upgrade/TransferCurrenciesCorrections.php | 55 ++++++++++++------- .../TransactionGroupRepositoryInterface.php | 2 + 43 files changed, 139 insertions(+), 209 deletions(-) diff --git a/app/Api/V1/Controllers/AccountController.php b/app/Api/V1/Controllers/AccountController.php index 9fa6170065..1a962df72f 100644 --- a/app/Api/V1/Controllers/AccountController.php +++ b/app/Api/V1/Controllers/AccountController.php @@ -227,10 +227,8 @@ class AccountController extends Controller if (null !== $limit && $limit > 0) { $pageSize = $limit; } - $types = $this->mapTransactionTypes($this->parameters->get('type')); $manager = $this->getManager(); - /** @var User $admin */ $admin = auth()->user(); @@ -240,7 +238,6 @@ class AccountController extends Controller $collector->setUser($admin)->setAccounts(new Collection([$account])) ->withAPIInformation()->setLimit($pageSize)->setPage($this->parameters->get('page'))->setTypes($types); - // set range if necessary: if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) { $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } diff --git a/app/Api/V1/Controllers/ConfigurationController.php b/app/Api/V1/Controllers/ConfigurationController.php index 23b78898a3..cf7423a30c 100644 --- a/app/Api/V1/Controllers/ConfigurationController.php +++ b/app/Api/V1/Controllers/ConfigurationController.php @@ -51,7 +51,6 @@ class ConfigurationController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { - /** @noinspection UnusedConstructorDependenciesInspection */ $this->repository = app(UserRepositoryInterface::class); /** @var User $admin */ $admin = auth()->user(); @@ -109,13 +108,12 @@ class ConfigurationController extends Controller $lastCheck = app('fireflyconfig')->get('last_update_check'); /** @var Configuration $singleUser */ $singleUser = app('fireflyconfig')->get('single_user_mode'); - $data = [ + + return [ 'is_demo_site' => null === $isDemoSite ? null : $isDemoSite->data, 'permission_update_check' => null === $updateCheck ? null : (int)$updateCheck->data, 'last_update_check' => null === $lastCheck ? null : (int)$lastCheck->data, 'single_user_mode' => null === $singleUser ? null : $singleUser->data, ]; - - return $data; } } diff --git a/app/Api/V1/Controllers/Search/AccountController.php b/app/Api/V1/Controllers/Search/AccountController.php index 72b44ce2ea..43ff8ffa5f 100644 --- a/app/Api/V1/Controllers/Search/AccountController.php +++ b/app/Api/V1/Controllers/Search/AccountController.php @@ -22,7 +22,6 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Search; - use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Support\Http\Api\AccountFilter; use FireflyIII\Support\Search\AccountSearch; @@ -98,5 +97,4 @@ class AccountController extends Controller return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); } - -} \ No newline at end of file +} diff --git a/app/Api/V1/Controllers/Search/TransactionController.php b/app/Api/V1/Controllers/Search/TransactionController.php index 78839e5e23..7febe475e8 100644 --- a/app/Api/V1/Controllers/Search/TransactionController.php +++ b/app/Api/V1/Controllers/Search/TransactionController.php @@ -22,11 +22,8 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Search; - use FireflyIII\Api\V1\Controllers\Controller; -use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\Response; /** * Class TransactionController @@ -60,9 +57,8 @@ class TransactionController extends Controller * * @return void */ - public function search(Request $request) + public function search(Request $request): void { die('the route is present but nobody\'s home.'); } - -} \ No newline at end of file +} diff --git a/app/Api/V1/Controllers/Search/TransferController.php b/app/Api/V1/Controllers/Search/TransferController.php index 587b8649ae..1fac6ba6f6 100644 --- a/app/Api/V1/Controllers/Search/TransferController.php +++ b/app/Api/V1/Controllers/Search/TransferController.php @@ -22,16 +22,13 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Search; - use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Search\TransferRequest; -use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Support\Search\TransferSearch; use FireflyIII\Transformers\TransactionGroupTransformer; use FireflyIII\User; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Http\Response; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Resource\Collection as FractalCollection; @@ -113,4 +110,4 @@ class TransferController extends Controller return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); } -} \ No newline at end of file +} diff --git a/app/Api/V1/Controllers/SummaryController.php b/app/Api/V1/Controllers/SummaryController.php index 15cd9c2f13..ca148ffab4 100644 --- a/app/Api/V1/Controllers/SummaryController.php +++ b/app/Api/V1/Controllers/SummaryController.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers; - use Carbon\Carbon; use Exception; use FireflyIII\Api\V1\Requests\DateRequest; @@ -124,7 +123,6 @@ class SummaryController extends Controller } return response()->json($return); - } /** @@ -151,25 +149,6 @@ class SummaryController extends Controller return $result; } - /** - * This method will scroll through the results of the spentInPeriodMc() array and return the correct info. - * - * @param array $spentInfo - * @param TransactionCurrency $currency - * - * @return string - */ - private function findInSpentArray(array $spentInfo, TransactionCurrency $currency): string - { - foreach ($spentInfo as $array) { - if ($array['currency_id'] === $currency->id) { - return (string)$array['amount']; - } - } - - return '0'; // @codeCoverageIgnore - } - /** * @param Carbon $start * @param Carbon $end @@ -197,7 +176,6 @@ class SummaryController extends Controller $set = $collector->getExtractedJournals(); /** @var array $transactionJournal */ foreach ($set as $transactionJournal) { - $currencyId = (int)$transactionJournal['currency_id']; $incomes[$currencyId] = $incomes[$currencyId] ?? '0'; $incomes[$currencyId] = bcadd($incomes[$currencyId], bcmul($transactionJournal['amount'], '-1')); @@ -373,12 +351,15 @@ class SummaryController extends Controller 'value_parsed' => app('amount')->formatFlat($row['currency_symbol'], $row['currency_decimal_places'], $leftToSpend, false), 'local_icon' => 'money', 'sub_title' => (string)trans( - 'firefly.box_spend_per_day', ['amount' => app('amount')->formatFlat( - $row['currency_symbol'], $row['currency_decimal_places'], $perDay, false - )] + 'firefly.box_spend_per_day', + ['amount' => app('amount')->formatFlat( + $row['currency_symbol'], + $row['currency_decimal_places'], + $perDay, + false + )] ), ]; - } return $return; } @@ -442,5 +423,4 @@ class SummaryController extends Controller return $return; } - } diff --git a/app/Api/V1/Controllers/TransactionController.php b/app/Api/V1/Controllers/TransactionController.php index 9e25876dcb..1cb04d03bf 100644 --- a/app/Api/V1/Controllers/TransactionController.php +++ b/app/Api/V1/Controllers/TransactionController.php @@ -110,7 +110,6 @@ class TransactionController extends Controller $resource = new FractalCollection($attachments, $transformer, 'attachments'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); - } /** @@ -215,7 +214,6 @@ class TransactionController extends Controller $resource = new FractalCollection($events, $transformer, 'piggy_bank_events'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); - } /** @@ -296,7 +294,7 @@ class TransactionController extends Controller ]; return response()->json($response, 422); - } catch(FireflyException $e) { + } catch (FireflyException $e) { Log::warning('Caught an exception. Return error message.'); Log::error($e->getMessage()); // return bad validation message. @@ -304,7 +302,7 @@ class TransactionController extends Controller $response = [ 'message' => 'The given data was invalid.', 'errors' => [ - 'transactions.0.description' => [sprintf('Internal exception: %s',$e->getMessage())], + 'transactions.0.description' => [sprintf('Internal exception: %s', $e->getMessage())] ], ]; @@ -379,6 +377,5 @@ class TransactionController extends Controller $resource = new Item($selectedGroup, $transformer, 'transactions'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); - } } diff --git a/app/Api/V1/Requests/AvailableBudgetRequest.php b/app/Api/V1/Requests/AvailableBudgetRequest.php index 8f66dcdcd4..077d077a44 100644 --- a/app/Api/V1/Requests/AvailableBudgetRequest.php +++ b/app/Api/V1/Requests/AvailableBudgetRequest.php @@ -64,15 +64,13 @@ class AvailableBudgetRequest extends Request */ public function rules(): array { - $rules = [ + return [ 'currency_id' => 'numeric|exists:transaction_currencies,id', 'currency_code' => 'min:3|max:3|exists:transaction_currencies,code', 'amount' => 'required|numeric|more:0', 'start' => 'required|date|before:end', 'end' => 'required|date|after:start', ]; - - return $rules; } diff --git a/app/Api/V1/Requests/BillRequest.php b/app/Api/V1/Requests/BillRequest.php index 31f5967d30..16bdefd292 100644 --- a/app/Api/V1/Requests/BillRequest.php +++ b/app/Api/V1/Requests/BillRequest.php @@ -60,7 +60,7 @@ class BillRequest extends Request $active = $this->boolean('active'); } - $data = [ + return [ 'name' => $this->string('name'), 'amount_min' => $this->string('amount_min'), 'amount_max' => $this->string('amount_max'), @@ -72,8 +72,6 @@ class BillRequest extends Request 'active' => $active, 'notes' => $this->nlString('notes'), ]; - - return $data; } /** diff --git a/app/Api/V1/Requests/RecurrenceStoreRequest.php b/app/Api/V1/Requests/RecurrenceStoreRequest.php index be57cb06c0..f5a399f76f 100644 --- a/app/Api/V1/Requests/RecurrenceStoreRequest.php +++ b/app/Api/V1/Requests/RecurrenceStoreRequest.php @@ -63,7 +63,8 @@ class RecurrenceStoreRequest extends Request if (null !== $this->get('apply_rules')) { $applyRules = $this->boolean('apply_rules'); } - $return = [ + + return [ 'recurrence' => [ 'type' => $this->string('type'), 'title' => $this->string('title'), @@ -77,8 +78,6 @@ class RecurrenceStoreRequest extends Request 'transactions' => $this->getTransactionData(), 'repetitions' => $this->getRepetitionData(), ]; - - return $return; } /** diff --git a/app/Api/V1/Requests/RecurrenceUpdateRequest.php b/app/Api/V1/Requests/RecurrenceUpdateRequest.php index 7397db39ef..6485a1b107 100644 --- a/app/Api/V1/Requests/RecurrenceUpdateRequest.php +++ b/app/Api/V1/Requests/RecurrenceUpdateRequest.php @@ -63,7 +63,8 @@ class RecurrenceUpdateRequest extends Request if (null !== $this->get('apply_rules')) { $applyRules = $this->boolean('apply_rules'); } - $return = [ + + return [ 'recurrence' => [ 'type' => $this->nullableString('type'), 'title' => $this->nullableString('title'), @@ -78,8 +79,6 @@ class RecurrenceUpdateRequest extends Request 'transactions' => $this->getTransactionData(), 'repetitions' => $this->getRepetitionData(), ]; - - return $return; } /** diff --git a/app/Api/V1/Requests/RuleGroupTestRequest.php b/app/Api/V1/Requests/RuleGroupTestRequest.php index 4f990d8b00..34f975217b 100644 --- a/app/Api/V1/Requests/RuleGroupTestRequest.php +++ b/app/Api/V1/Requests/RuleGroupTestRequest.php @@ -54,7 +54,7 @@ class RuleGroupTestRequest extends Request */ public function getTestParameters(): array { - $return = [ + return [ 'page' => $this->getPage(), 'start_date' => $this->getDate('start_date'), 'end_date' => $this->getDate('end_date'), @@ -62,9 +62,6 @@ class RuleGroupTestRequest extends Request 'trigger_limit' => $this->getTriggerLimit(), 'accounts' => $this->getAccounts(), ]; - - - return $return; } /** diff --git a/app/Api/V1/Requests/RuleGroupTriggerRequest.php b/app/Api/V1/Requests/RuleGroupTriggerRequest.php index 476009b79c..059eede3be 100644 --- a/app/Api/V1/Requests/RuleGroupTriggerRequest.php +++ b/app/Api/V1/Requests/RuleGroupTriggerRequest.php @@ -54,14 +54,11 @@ class RuleGroupTriggerRequest extends Request */ public function getTriggerParameters(): array { - $return = [ + return [ 'start_date' => $this->getDate('start_date'), 'end_date' => $this->getDate('end_date'), 'accounts' => $this->getAccounts(), ]; - - - return $return; } /** diff --git a/app/Api/V1/Requests/RuleStoreRequest.php b/app/Api/V1/Requests/RuleStoreRequest.php index dc7fb256f9..5c275c60d7 100644 --- a/app/Api/V1/Requests/RuleStoreRequest.php +++ b/app/Api/V1/Requests/RuleStoreRequest.php @@ -65,7 +65,7 @@ class RuleStoreRequest extends Request $stopProcessing = $this->boolean('stop_processing'); } - $data = [ + return [ 'title' => $this->string('title'), 'description' => $this->string('description'), 'rule_group_id' => $this->integer('rule_group_id'), @@ -77,8 +77,6 @@ class RuleStoreRequest extends Request 'triggers' => $this->getRuleTriggers(), 'actions' => $this->getRuleActions(), ]; - - return $data; } /** @@ -94,7 +92,8 @@ class RuleStoreRequest extends Request // some triggers and actions require text: $contextTriggers = implode(',', config('firefly.context-rule-triggers')); $contextActions = implode(',', config('firefly.context-rule-actions')); - $rules = [ + + return [ 'title' => 'required|between:1,100|uniqueObjectForUser:rules,title', 'description' => 'between:1,5000|nullable', 'rule_group_id' => 'required|belongsToUser:rule_groups|required_without:rule_group_title', @@ -112,8 +111,6 @@ class RuleStoreRequest extends Request 'stop_processing' => [new IsBoolean], 'active' => [new IsBoolean], ]; - - return $rules; } /** diff --git a/app/Api/V1/Requests/RuleTestRequest.php b/app/Api/V1/Requests/RuleTestRequest.php index 3aa41ac848..ff481c33d0 100644 --- a/app/Api/V1/Requests/RuleTestRequest.php +++ b/app/Api/V1/Requests/RuleTestRequest.php @@ -54,7 +54,7 @@ class RuleTestRequest extends Request */ public function getTestParameters(): array { - $return = [ + return [ 'page' => $this->getPage(), 'start_date' => $this->getDate('start_date'), 'end_date' => $this->getDate('end_date'), @@ -62,9 +62,6 @@ class RuleTestRequest extends Request 'trigger_limit' => $this->getTriggerLimit(), 'accounts' => $this->getAccounts(), ]; - - - return $return; } /** diff --git a/app/Api/V1/Requests/RuleTriggerRequest.php b/app/Api/V1/Requests/RuleTriggerRequest.php index 8d4042b0b8..28a2cd9c04 100644 --- a/app/Api/V1/Requests/RuleTriggerRequest.php +++ b/app/Api/V1/Requests/RuleTriggerRequest.php @@ -53,14 +53,11 @@ class RuleTriggerRequest extends Request */ public function getTriggerParameters(): array { - $return = [ + return [ 'start_date' => $this->getDate('start_date'), 'end_date' => $this->getDate('end_date'), 'accounts' => $this->getAccounts(), ]; - - - return $return; } /** diff --git a/app/Api/V1/Requests/RuleUpdateRequest.php b/app/Api/V1/Requests/RuleUpdateRequest.php index 15d05c0032..d8c73d915d 100644 --- a/app/Api/V1/Requests/RuleUpdateRequest.php +++ b/app/Api/V1/Requests/RuleUpdateRequest.php @@ -65,7 +65,7 @@ class RuleUpdateRequest extends Request $stopProcessing = $this->boolean('stop_processing'); } - $data = [ + return [ 'title' => $this->nullableString('title'), 'description' => $this->nullableString('description'), 'rule_group_id' => $this->nullableInteger('rule_group_id'), @@ -77,8 +77,6 @@ class RuleUpdateRequest extends Request 'triggers' => $this->getRuleTriggers(), 'actions' => $this->getRuleActions(), ]; - - return $data; } /** @@ -95,7 +93,8 @@ class RuleUpdateRequest extends Request // some triggers and actions require text: $contextTriggers = implode(',', config('firefly.context-rule-triggers')); $contextActions = implode(',', config('firefly.context-rule-actions')); - $rules = [ + + return [ 'title' => sprintf('between:1,100|uniqueObjectForUser:rules,title,%d', $rule->id), 'description' => 'between:1,5000|nullable', 'rule_group_id' => 'belongsToUser:rule_groups', @@ -113,8 +112,6 @@ class RuleUpdateRequest extends Request 'stop_processing' => [new IsBoolean], 'active' => [new IsBoolean], ]; - - return $rules; } /** diff --git a/app/Api/V1/Requests/Search/TransferRequest.php b/app/Api/V1/Requests/Search/TransferRequest.php index 52a046170c..5cfb4d3ead 100644 --- a/app/Api/V1/Requests/Search/TransferRequest.php +++ b/app/Api/V1/Requests/Search/TransferRequest.php @@ -22,7 +22,6 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Requests\Search; - use FireflyIII\Api\V1\Requests\Request; use FireflyIII\Rules\IsTransferAccount; @@ -55,5 +54,4 @@ class TransferRequest extends Request 'date' => 'required|date', ]; } - -} \ No newline at end of file +} diff --git a/app/Api/V1/Requests/TransactionStoreRequest.php b/app/Api/V1/Requests/TransactionStoreRequest.php index 3b9319dd51..9d34e8ac0c 100644 --- a/app/Api/V1/Requests/TransactionStoreRequest.php +++ b/app/Api/V1/Requests/TransactionStoreRequest.php @@ -60,14 +60,13 @@ class TransactionStoreRequest extends Request public function getAll(): array { Log::debug('get all data in TransactionStoreRequest'); - $data = [ + + return [ 'group_title' => $this->string('group_title'), 'error_if_duplicate_hash' => $this->boolean('error_if_duplicate_hash'), 'apply_rules' => $this->boolean('apply_rules', true), 'transactions' => $this->getTransactionData(), ]; - - return $data; } /** @@ -78,7 +77,8 @@ class TransactionStoreRequest extends Request public function rules(): array { Log::debug('Collect rules of TransactionStoreRequest'); - $rules = [ + + return [ // basic fields for group: 'group_title' => 'between:1,1000|nullable', 'error_if_duplicate_hash' => [new IsBoolean], @@ -156,8 +156,6 @@ class TransactionStoreRequest extends Request 'transactions.*.invoice_date' => 'date|nullable', ]; - return $rules; - } diff --git a/app/Api/V1/Requests/TransactionUpdateRequest.php b/app/Api/V1/Requests/TransactionUpdateRequest.php index 422032ba3e..11ccc8598a 100644 --- a/app/Api/V1/Requests/TransactionUpdateRequest.php +++ b/app/Api/V1/Requests/TransactionUpdateRequest.php @@ -154,7 +154,7 @@ class TransactionUpdateRequest extends Request */ public function rules(): array { - $rules = [ + return [ // basic fields for group: 'group_title' => 'between:1,1000', 'apply_rules' => [new IsBoolean], @@ -222,8 +222,6 @@ class TransactionUpdateRequest extends Request 'transactions.*.payment_date' => 'date|nullable', 'transactions.*.invoice_date' => 'date|nullable', ]; - - return $rules; } /** diff --git a/app/Api/V1/Requests/UserStoreRequest.php b/app/Api/V1/Requests/UserStoreRequest.php index 89680806d0..e9b55980fe 100644 --- a/app/Api/V1/Requests/UserStoreRequest.php +++ b/app/Api/V1/Requests/UserStoreRequest.php @@ -69,14 +69,13 @@ class UserStoreRequest extends Request if (null !== $this->get('blocked')) { $blocked = $this->boolean('blocked'); } - $data = [ + + return [ 'email' => $this->string('email'), 'blocked' => $blocked, 'blocked_code' => $this->string('blocked_code'), 'role' => $this->string('role'), ]; - - return $data; } /** diff --git a/app/Api/V1/Requests/UserUpdateRequest.php b/app/Api/V1/Requests/UserUpdateRequest.php index bc3fe9d3bb..ce85ead64f 100644 --- a/app/Api/V1/Requests/UserUpdateRequest.php +++ b/app/Api/V1/Requests/UserUpdateRequest.php @@ -69,14 +69,13 @@ class UserUpdateRequest extends Request if (null !== $this->get('blocked')) { $blocked = $this->boolean('blocked'); } - $data = [ + + return [ 'email' => $this->string('email'), 'blocked' => $blocked, 'blocked_code' => $this->string('blocked_code'), 'role' => $this->string('role'), ]; - - return $data; } /** @@ -87,14 +86,13 @@ class UserUpdateRequest extends Request public function rules(): array { $user = $this->route()->parameter('user'); - $rules = [ + + return [ 'email' => sprintf('email|unique:users,email,%d', $user->id), 'blocked' => [new IsBoolean], 'blocked_code' => 'in:email_changed', 'role' => 'in:owner,demo,', ]; - - return $rules; } } diff --git a/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php b/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php index fda42940a7..6b1fc6a04f 100644 --- a/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php +++ b/app/Console/Commands/Correction/CorrectOpeningBalanceCurrencies.php @@ -111,15 +111,12 @@ class CorrectOpeningBalanceCurrencies extends Command */ private function getAccount(TransactionJournal $journal): ?Account { - $excluded = []; $transactions = $journal->transactions()->with(['account', 'account.accountType'])->get(); /** @var Transaction $transaction */ foreach ($transactions as $transaction) { $account = $transaction->account; - if (null !== $account) { - if (AccountType::INITIAL_BALANCE !== $account->accountType->type) { - return $account; - } + if ((null !== $account) && AccountType::INITIAL_BALANCE !== $account->accountType->type) { + return $account; } } diff --git a/app/Console/Commands/Correction/DeleteEmptyGroups.php b/app/Console/Commands/Correction/DeleteEmptyGroups.php index a9a640de36..f03796d3ce 100644 --- a/app/Console/Commands/Correction/DeleteEmptyGroups.php +++ b/app/Console/Commands/Correction/DeleteEmptyGroups.php @@ -25,7 +25,6 @@ namespace FireflyIII\Console\Commands\Correction; use Exception; use FireflyIII\Models\TransactionGroup; -use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; use Log; @@ -59,7 +58,7 @@ class DeleteEmptyGroups extends Command $start = microtime(true); $groupIds = TransactionGroup - ::leftJoin('transaction_journals','transaction_groups.id','=','transaction_journals.transaction_group_id') + ::leftJoin('transaction_journals', 'transaction_groups.id', '=', 'transaction_journals.transaction_group_id') ->whereNull('transaction_journals.id')->get(['transaction_groups.id'])->pluck('id')->toArray(); $total = count($groupIds); diff --git a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php b/app/Console/Commands/Correction/DeleteOrphanedTransactions.php index b9611e599c..f741da152e 100644 --- a/app/Console/Commands/Correction/DeleteOrphanedTransactions.php +++ b/app/Console/Commands/Correction/DeleteOrphanedTransactions.php @@ -91,8 +91,11 @@ class DeleteOrphanedTransactions extends Command } Transaction::where('transaction_journal_id', (int)$transaction->transaction_journal_id)->delete(); $this->line( - sprintf('Deleted transaction journal #%d because account #%d was already deleted.', - $transaction->transaction_journal_id, $transaction->account_id) + sprintf( + 'Deleted transaction journal #%d because account #%d was already deleted.', + $transaction->transaction_journal_id, + $transaction->account_id + ) ); $count++; } @@ -134,6 +137,5 @@ class DeleteOrphanedTransactions extends Command if (0 === $count) { $this->info('No orphaned transactions.'); } - } } diff --git a/app/Console/Commands/Correction/FixAccountTypes.php b/app/Console/Commands/Correction/FixAccountTypes.php index 277d72a0df..6f1e8adb4d 100644 --- a/app/Console/Commands/Correction/FixAccountTypes.php +++ b/app/Console/Commands/Correction/FixAccountTypes.php @@ -167,9 +167,12 @@ class FixAccountTypes extends Command $dest->save(); $this->info( sprintf( - 'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").', $journal->id, - $oldDest->id, $oldDest->name, - $result->id, $result->name + 'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").', + $journal->id, + $oldDest->id, + $oldDest->name, + $result->id, + $result->name ) ); $this->inspectJournal($journal); @@ -184,9 +187,12 @@ class FixAccountTypes extends Command $source->save(); $this->info( sprintf( - 'Transaction journal #%d, source account changed from #%d ("%s") to #%d ("%s").', $journal->id, - $oldSource->id, $oldSource->name, - $result->id, $result->name + 'Transaction journal #%d, source account changed from #%d ("%s") to #%d ("%s").', + $journal->id, + $oldSource->id, + $oldSource->name, + $result->id, + $result->name ) ); $this->inspectJournal($journal); @@ -198,7 +204,6 @@ class FixAccountTypes extends Command break; } - } /** @@ -273,5 +278,4 @@ class FixAccountTypes extends Command $this->fixJournal($journal, $type, $sourceTransaction, $destTransaction); } } - } diff --git a/app/Console/Commands/Correction/FixRecurringTransactions.php b/app/Console/Commands/Correction/FixRecurringTransactions.php index f9e99f6de1..c64039a6f8 100644 --- a/app/Console/Commands/Correction/FixRecurringTransactions.php +++ b/app/Console/Commands/Correction/FixRecurringTransactions.php @@ -51,16 +51,6 @@ class FixRecurringTransactions extends Command /** @var UserRepositoryInterface */ private $userRepos; - /** - * Create a new command instance. - * - * @return void - */ - public function __construct() - { - parent::__construct(); - } - /** * Execute the console command. * diff --git a/app/Console/Commands/Correction/FixUnevenAmount.php b/app/Console/Commands/Correction/FixUnevenAmount.php index 2001cf0848..6e7ead58a5 100644 --- a/app/Console/Commands/Correction/FixUnevenAmount.php +++ b/app/Console/Commands/Correction/FixUnevenAmount.php @@ -94,7 +94,8 @@ class FixUnevenAmount extends Command if (null === $source) { $this->error( sprintf( - 'Journal #%d ("%s") has no source transaction. It will be deleted to maintain database consistency.', $journal->id ?? 0, + 'Journal #%d ("%s") has no source transaction. It will be deleted to maintain database consistency.', + $journal->id ?? 0, $journal->description ?? '' ) ); @@ -113,7 +114,8 @@ class FixUnevenAmount extends Command if (null === $destination) { $this->error( sprintf( - 'Journal #%d ("%s") has no destination transaction. It will be deleted to maintain database consistency.', $journal->id ?? 0, + 'Journal #%d ("%s") has no destination transaction. It will be deleted to maintain database consistency.', + $journal->id ?? 0, $journal->description ?? '' ) ); diff --git a/app/Console/Commands/DecryptDatabase.php b/app/Console/Commands/DecryptDatabase.php index 24885712c2..a3733fd1bf 100644 --- a/app/Console/Commands/DecryptDatabase.php +++ b/app/Console/Commands/DecryptDatabase.php @@ -92,7 +92,7 @@ class DecryptDatabase extends Command // A separate routine for preferences: if ('preferences' === $table) { // try to json_decrypt the value. - $value = json_decode($value, true) ?? $value; + $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR) ?? $value; Log::debug(sprintf('Decrypted field "%s" "%s" to "%s" in table "%s" (row #%d)', $field, $original, print_r($value, true), $table, $id)); /** @var Preference $object */ diff --git a/app/Console/Commands/Export/ExportData.php b/app/Console/Commands/Export/ExportData.php index b2df8e08b4..017c30a505 100644 --- a/app/Console/Commands/Export/ExportData.php +++ b/app/Console/Commands/Export/ExportData.php @@ -76,21 +76,12 @@ class ExportData extends Command /** @var User */ private $user; - /** - * Create a new command instance. - * - * @return void - */ - public function __construct() - { - parent::__construct(); - } - /** * Execute the console command. * * @return int * @throws FireflyException + * @throws \League\Csv\CannotInsertRecord */ public function handle(): int { @@ -206,6 +197,7 @@ class ExportData extends Command * * @return Carbon * @throws FireflyException + * @throws \Exception */ private function getDateParameter(string $field): Carbon { diff --git a/app/Console/Commands/Import/CreateCSVImport.php b/app/Console/Commands/Import/CreateCSVImport.php index 5583a71c77..d402271c27 100644 --- a/app/Console/Commands/Import/CreateCSVImport.php +++ b/app/Console/Commands/Import/CreateCSVImport.php @@ -93,7 +93,7 @@ class CreateCSVImport extends Command $this->importRepository->setUser($user); - $configurationData = json_decode(file_get_contents($configuration), true); + $configurationData = json_decode(file_get_contents($configuration), true, 512, JSON_THROW_ON_ERROR); $this->importJob = $this->importRepository->create('file'); @@ -214,7 +214,7 @@ class CreateCSVImport extends Command return false; } - $configurationData = json_decode(file_get_contents($configuration), true); + $configurationData = json_decode(file_get_contents($configuration), true, 512, JSON_THROW_ON_ERROR); if (null === $configurationData) { $this->errorLine(sprintf('Firefly III cannot read the contents of configuration file "%s" (working directory: "%s").', $configuration, $cwd)); diff --git a/app/Console/Commands/Integrity/RestoreOAuthKeys.php b/app/Console/Commands/Integrity/RestoreOAuthKeys.php index 67950d8798..ec5f0d9d67 100644 --- a/app/Console/Commands/Integrity/RestoreOAuthKeys.php +++ b/app/Console/Commands/Integrity/RestoreOAuthKeys.php @@ -21,8 +21,6 @@ namespace FireflyIII\Console\Commands\Integrity; -use Artisan; -use Crypt; use FireflyIII\Support\System\OAuthKeys; use Illuminate\Console\Command; @@ -122,4 +120,4 @@ class RestoreOAuthKeys extends Command { OAuthKeys::storeKeysInDB(); } -} \ No newline at end of file +} diff --git a/app/Console/Commands/ScanAttachments.php b/app/Console/Commands/ScanAttachments.php index 4546c8c646..60077a1573 100644 --- a/app/Console/Commands/ScanAttachments.php +++ b/app/Console/Commands/ScanAttachments.php @@ -64,7 +64,6 @@ class ScanAttachments extends Command /** @var Attachment $attachment */ foreach ($attachments as $attachment) { $fileName = $attachment->fileName(); - $decryptedContent = ''; try { $encryptedContent = $disk->get($fileName); } catch (FileNotFoundException $e) { diff --git a/app/Console/Commands/SetLatestVersion.php b/app/Console/Commands/SetLatestVersion.php index 4eb0ce6285..f18441793a 100644 --- a/app/Console/Commands/SetLatestVersion.php +++ b/app/Console/Commands/SetLatestVersion.php @@ -43,16 +43,6 @@ class SetLatestVersion extends Command */ protected $signature = 'firefly-iii:set-latest-version {--james-is-cool}'; - /** - * Create a new command instance. - * - * @return void - */ - public function __construct() - { - parent::__construct(); - } - /** * Execute the console command. * diff --git a/app/Console/Commands/Tools/Cron.php b/app/Console/Commands/Tools/Cron.php index b3e9974ff5..1f2b77ce78 100644 --- a/app/Console/Commands/Tools/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -100,9 +100,11 @@ class Cron extends Command /** * @param bool $force * @param Carbon|null $date + * * @throws FireflyException + * @throws Exception */ - private function autoBudgetCronJob(bool $force, ?Carbon $date) + private function autoBudgetCronJob(bool $force, ?Carbon $date): void { $autoBudget = new AutoBudgetCronjob; $autoBudget->setForce($force); diff --git a/app/Console/Commands/Upgrade/AccountCurrencies.php b/app/Console/Commands/Upgrade/AccountCurrencies.php index 4ac42aaaa6..cbb6d77b1b 100644 --- a/app/Console/Commands/Upgrade/AccountCurrencies.php +++ b/app/Console/Commands/Upgrade/AccountCurrencies.php @@ -178,7 +178,8 @@ class AccountCurrencies extends Command static function (Transaction $transaction) use ($accountCurrency) { $transaction->transaction_currency_id = $accountCurrency; $transaction->save(); - }); + } + ); $this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name)); $this->count++; diff --git a/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php b/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php index 317a9dd157..396a2a27b7 100644 --- a/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php +++ b/app/Console/Commands/Upgrade/MigrateRecurrenceMeta.php @@ -115,7 +115,7 @@ class MigrateRecurrenceMeta extends Command if ('tags' === $meta->name) { $array = explode(',', $meta->value); - $value = json_encode($array); + $value = json_encode($array, JSON_THROW_ON_ERROR, 512); } RecurrenceTransactionMeta::create( diff --git a/app/Console/Commands/Upgrade/MigrateToGroups.php b/app/Console/Commands/Upgrade/MigrateToGroups.php index 4645a4e404..c0f1c68f94 100644 --- a/app/Console/Commands/Upgrade/MigrateToGroups.php +++ b/app/Console/Commands/Upgrade/MigrateToGroups.php @@ -302,7 +302,8 @@ class MigrateToGroups extends Command $this->error( sprintf( 'Journal #%d has no opposing transaction for transaction #%d. Cannot upgrade this entry.', - $journal->id, $transaction->id + $journal->id, + $transaction->id ) ); continue; @@ -365,12 +366,20 @@ class MigrateToGroups extends Command // report on result: Log::debug( - sprintf('Migrated journal #%d into group #%d with these journals: #%s', - $journal->id, $group->id, implode(', #', $group->transactionJournals->pluck('id')->toArray())) + sprintf( + 'Migrated journal #%d into group #%d with these journals: #%s', + $journal->id, + $group->id, + implode(', #', $group->transactionJournals->pluck('id')->toArray()) + ) ); $this->line( - sprintf('Migrated journal #%d into group #%d with these journals: #%s', - $journal->id, $group->id, implode(', #', $group->transactionJournals->pluck('id')->toArray())) + sprintf( + 'Migrated journal #%d into group #%d with these journals: #%s', + $journal->id, + $group->id, + implode(', #', $group->transactionJournals->pluck('id')->toArray()) + ) ); } @@ -447,5 +456,4 @@ class MigrateToGroups extends Command { app('fireflyconfig')->set(self::CONFIG_NAME, true); } - } diff --git a/app/Console/Commands/Upgrade/MigrateToRules.php b/app/Console/Commands/Upgrade/MigrateToRules.php index 0286d4b70f..f1ecbd56ec 100644 --- a/app/Console/Commands/Upgrade/MigrateToRules.php +++ b/app/Console/Commands/Upgrade/MigrateToRules.php @@ -177,9 +177,9 @@ class MigrateToRules extends Command } /** - * @param RuleGroup $ruleGroup - * @param Bill $bill - * @throws FireflyException + * @param RuleGroup $ruleGroup + * @param Bill $bill + * @param Preference $language */ private function migrateBill(RuleGroup $ruleGroup, Bill $bill, Preference $language): void { diff --git a/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php b/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php index daa00621bd..7a6a37e5b9 100644 --- a/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php +++ b/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php @@ -40,7 +40,6 @@ use Illuminate\Console\Command; */ class OtherCurrenciesCorrections extends Command { - public const CONFIG_NAME = '480_other_currencies'; /** * The console command description. @@ -136,8 +135,6 @@ class OtherCurrenciesCorrections extends Command $this->accountCurrencies[$accountId] = $currency; return $currency; - - } /** @@ -186,8 +183,12 @@ class OtherCurrenciesCorrections extends Command $currency = $this->getCurrency($account); if (null === $currency) { // @codeCoverageIgnoreStart - $this->error(sprintf('Account #%d ("%s") has no currency preference, so transaction journal #%d can\'t be corrected', - $account->id, $account->name, $journal->id)); + $this->error(sprintf( + 'Account #%d ("%s") has no currency preference, so transaction journal #%d can\'t be corrected', + $account->id, + $account->name, + $journal->id + )); $this->count++; return; diff --git a/app/Console/Commands/Upgrade/RenameAccountMeta.php b/app/Console/Commands/Upgrade/RenameAccountMeta.php index 493fbd2f97..fef408cc3a 100644 --- a/app/Console/Commands/Upgrade/RenameAccountMeta.php +++ b/app/Console/Commands/Upgrade/RenameAccountMeta.php @@ -76,7 +76,7 @@ class RenameAccountMeta extends Command $count += AccountMeta::where('name', $old)->update(['name' => $new]); // delete empty entries while we're at it. - AccountMeta::where('name', $new)->where('data','""')->delete(); + AccountMeta::where('name', $new)->where('data', '""')->delete(); } $this->markAsExecuted(); diff --git a/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php b/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php index af19d78ad2..13a674112a 100644 --- a/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php +++ b/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands\Upgrade; - use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; @@ -41,7 +40,6 @@ use Log; */ class TransferCurrenciesCorrections extends Command { - public const CONFIG_NAME = '480_transfer_currencies'; /** * The console command description. @@ -162,8 +160,6 @@ class TransferCurrenciesCorrections extends Command $this->accountCurrencies[$accountId] = $result; return $result; - - } /** @@ -297,7 +293,6 @@ class TransferCurrenciesCorrections extends Command */ private function updateTransferCurrency(TransactionJournal $transfer): void { - $this->resetInformation(); // @codeCoverageIgnoreStart @@ -325,7 +320,8 @@ class TransferCurrenciesCorrections extends Command // @codeCoverageIgnoreStart if ($this->isNoCurrencyPresent()) { $this->error( - sprintf('Source or destination accounts for transaction journal #%d have no currency information. Cannot fix this one.', $transfer->id)); + sprintf('Source or destination accounts for transaction journal #%d have no currency information. Cannot fix this one.', $transfer->id) + ); return; } @@ -355,7 +351,6 @@ class TransferCurrenciesCorrections extends Command // fix journal itself: $this->fixTransactionJournalCurrency($transfer); - } /** @@ -367,8 +362,11 @@ class TransferCurrenciesCorrections extends Command if (null === $this->sourceTransaction->transaction_currency_id && null !== $this->sourceCurrency) { $this->sourceTransaction ->transaction_currency_id = (int)$this->sourceCurrency->id; - $message = sprintf('Transaction #%d has no currency setting, now set to %s.', - $this->sourceTransaction->id, $this->sourceCurrency->code); + $message = sprintf( + 'Transaction #%d has no currency setting, now set to %s.', + $this->sourceTransaction->id, + $this->sourceCurrency->code + ); Log::debug($message); $this->line($message); $this->count++; @@ -385,8 +383,11 @@ class TransferCurrenciesCorrections extends Command if (null === $this->destinationTransaction->transaction_currency_id && null !== $this->destinationCurrency) { $this->destinationTransaction ->transaction_currency_id = (int)$this->destinationCurrency->id; - $message = sprintf('Transaction #%d has no currency setting, now set to %s.', - $this->destinationTransaction->id, $this->destinationCurrency->code); + $message = sprintf( + 'Transaction #%d has no currency setting, now set to %s.', + $this->destinationTransaction->id, + $this->destinationCurrency->code + ); Log::debug($message); $this->line($message); $this->count++; @@ -490,9 +491,13 @@ class TransferCurrenciesCorrections extends Command sprintf( 'Currency for account "%s" is %s, and currency for account "%s" is also %s, so transactions #%d and #%d has been verified to be to %s exclusively.', - $this->destinationAccount->name, $this->destinationCurrency->code, - $this->sourceAccount->name, $this->sourceCurrency->code, - $this->sourceTransaction->id, $this->destinationTransaction->id, $this->sourceCurrency->code + $this->destinationAccount->name, + $this->destinationCurrency->code, + $this->sourceAccount->name, + $this->sourceCurrency->code, + $this->sourceTransaction->id, + $this->destinationTransaction->id, + $this->sourceCurrency->code ) ); } @@ -528,8 +533,11 @@ class TransferCurrenciesCorrections extends Command $this->sourceTransaction->foreign_amount = bcmul((string)$this->destinationTransaction->foreign_amount, '-1'); $this->sourceTransaction->save(); $this->count++; - Log::debug(sprintf('Restored foreign amount of source transaction #%d to %s', - $this->sourceTransaction->id, $this->sourceTransaction->foreign_amount)); + Log::debug(sprintf( + 'Restored foreign amount of source transaction #%d to %s', + $this->sourceTransaction->id, + $this->sourceTransaction->foreign_amount + )); } } @@ -543,8 +551,11 @@ class TransferCurrenciesCorrections extends Command $this->destinationTransaction->foreign_amount = bcmul((string)$this->sourceTransaction->foreign_amount, '-1'); $this->destinationTransaction->save(); $this->count++; - Log::debug(sprintf('Restored foreign amount of destination transaction #%d to %s', - $this->destinationTransaction->id, $this->destinationTransaction->foreign_amount)); + Log::debug(sprintf( + 'Restored foreign amount of destination transaction #%d to %s', + $this->destinationTransaction->id, + $this->destinationTransaction->foreign_amount + )); } } @@ -565,8 +576,11 @@ class TransferCurrenciesCorrections extends Command // destination account must have a currency preference. if (null === $this->destinationCurrency) { - $message = sprintf('Account #%d ("%s") must have currency preference but has none.', - $this->destinationAccount->id, $this->destinationAccount->name); + $message = sprintf( + 'Account #%d ("%s") must have currency preference but has none.', + $this->destinationAccount->id, + $this->destinationAccount->name + ); Log::error($message); $this->error($message); @@ -575,5 +589,4 @@ class TransferCurrenciesCorrections extends Command return false; } - } diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php b/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php index ce0a7e0be1..014f49e7d8 100644 --- a/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php +++ b/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\TransactionGroup; use FireflyIII\Exceptions\DuplicateTransactionException; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\TransactionGroup; use FireflyIII\Support\NullArrayObject; use FireflyIII\User; @@ -135,6 +136,7 @@ interface TransactionGroupRepositoryInterface * * @return TransactionGroup * @throws DuplicateTransactionException + * @throws FireflyException */ public function store(array $data): TransactionGroup; From d7e953d38c51e96a9159bf758312e47a2d6b94e3 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 15 Mar 2020 09:54:44 +0100 Subject: [PATCH 23/53] Clean up complicated method. --- .../TransactionGroupTransformer.php | 334 +++++++++++++----- 1 file changed, 238 insertions(+), 96 deletions(-) diff --git a/app/Transformers/TransactionGroupTransformer.php b/app/Transformers/TransactionGroupTransformer.php index 4731f56e31..336c30aafc 100644 --- a/app/Transformers/TransactionGroupTransformer.php +++ b/app/Transformers/TransactionGroupTransformer.php @@ -24,9 +24,11 @@ declare(strict_types=1); namespace FireflyIII\Transformers; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; @@ -119,16 +121,119 @@ class TransactionGroupTransformer extends AbstractTransformer ], ], ]; - } catch(FireflyException $e) { + } catch (FireflyException $e) { Log::error($e->getMessage()); Log::error($e->getTraceAsString()); throw new FireflyException(sprintf('Transaction group #%d is broken. Please check out your log files.', $group->id)); } + // do something else. return $result; } + /** + * @param string $type + * @param string $amount + * + * @return string + */ + private function getAmount(string $type, string $amount): string + { + $amount = app('steam')->positive($amount); + if (TransactionType::WITHDRAWAL !== $type) { + $amount = app('steam')->negative($amount); + } + + return $amount; + } + + /** + * @param Bill|null $bill + * + * @return array + */ + private function getBill(?Bill $bill): array + { + $array = [ + 'id' => null, + 'name' => null, + ]; + if (null === $bill) { + return $array; + } + $array['id'] = $bill->id; + $array['name'] = $bill->name; + + return $array; + } + + /** + * @param Budget|null $budget + * + * @return array + */ + private function getBudget(?Budget $budget): array + { + $array = [ + 'id' => null, + 'name' => null, + ]; + if (null === $budget) { + return $array; + } + $array['id'] = $budget->id; + $array['name'] = $budget->name; + + return $array; + } + + /** + * @param Category|null $category + * + * @return array + */ + private function getCategory(?Category $category): array + { + $array = [ + 'id' => null, + 'name' => null, + ]; + if (null === $category) { + return $array; + } + $array['id'] = $category->id; + $array['name'] = $category->name; + + return $array; + } + + /** + * @param NullArrayObject $dates + * + * @return array + */ + private function getDates(NullArrayObject $dates): array + { + $fields = [ + 'interest_date', + 'book_date', + 'process_date', + 'due_date', + 'payment_date', + 'invoice_date', + ]; + $return = []; + foreach ($fields as $field) { + $return[$field] = null; + if (null !== $dates[$field]) { + $return[$field] = $dates[$field]->toAtomString(); + } + } + + return $return; + } + /** * @param TransactionJournal $journal * @@ -149,6 +254,46 @@ class TransactionGroupTransformer extends AbstractTransformer return $result; } + /** + * @param string $type + * @param string|null $foreignAmount + * + * @return string|null + */ + private function getForeignAmount(string $type, ?string $foreignAmount): ?string + { + $result = null; + if (null !== $foreignAmount) { + $result = TransactionType::WITHDRAWAL !== $type ? app('steam')->negative($foreignAmount) : app('steam')->positive($foreignAmount); + } + + return $result; + } + + /** + * @param TransactionCurrency|null $currency + * + * @return array + */ + private function getForeignCurrency(?TransactionCurrency $currency): array + { + $array = [ + 'id' => null, + 'code' => null, + 'symbol' => null, + 'decimal_places' => null, + ]; + if (null === $currency) { + return $array; + } + $array['id'] = $currency->id; + $array['code'] = $currency->code; + $array['symbol'] = $currency->symbol; + $array['decimal_places'] = $currency->decimal_places; + + return $array; + } + /** * @param TransactionJournal $journal * @@ -169,6 +314,97 @@ class TransactionGroupTransformer extends AbstractTransformer return $result; } + /** + * @param TransactionJournal $journal + * + * @return array + * @throws FireflyException + */ + private function transformJournal(TransactionJournal $journal): array + { + $source = $this->getSourceTransaction($journal); + $destination = $this->getDestinationTransaction($journal); + $type = $journal->transactionType->type; + $amount = $this->getAmount($type, $source->amount); + $foreignAmount = $this->getForeignAmount($type, $source->foreign_amount); + $metaFieldData = $this->groupRepos->getMetaFields($journal->id, $this->metaFields); + $metaDates = $this->getDates($this->groupRepos->getMetaDateFields($journal->id, $this->metaDateFields)); + $currency = $source->transactionCurrency; + $foreignCurrency = $this->getForeignCurrency($source->foreignCurrency); + $budget = $this->getBudget($journal->budgets->first()); + $category = $this->getCategory($journal->categories->first()); + $bill = $this->getBill($journal->bill); + + return [ + 'user' => (int)$journal->user_id, + 'transaction_journal_id' => $journal->id, + 'type' => strtolower($type), + 'date' => $journal->date->toAtomString(), + 'order' => $journal->order, + + 'currency_id' => $currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => $currency->decimal_places, + + 'foreign_currency_id' => $foreignCurrency['id'], + 'foreign_currency_code' => $foreignCurrency['code'], + 'foreign_currency_symbol' => $foreignCurrency['symbol'], + 'foreign_currency_decimal_places' => $foreignCurrency['decimal_places'], + + 'amount' => $amount, + 'foreign_amount' => $foreignAmount, + + 'description' => $journal->description, + + 'source_id' => $source->account_id, + 'source_name' => $source->account->name, + 'source_iban' => $source->account->iban, + 'source_type' => $source->account->accountType->type, + + 'destination_id' => $destination->account_id, + 'destination_name' => $destination->account->name, + 'destination_iban' => $destination->account->iban, + 'destination_type' => $destination->account->accountType->type, + + 'budget_id' => $budget['id'], + 'budget_name' => $budget['name'], + + 'category_id' => $category['id'], + 'category_name' => $category['name'], + + 'bill_id' => $bill['id'], + 'bill_name' => $bill['name'], + + 'reconciled' => $source->reconciled, + 'notes' => $this->groupRepos->getNoteText($journal->id), + 'tags' => $this->groupRepos->getTags($journal->id), + + 'internal_reference' => $metaFieldData['internal_reference'], + 'external_id' => $metaFieldData['external_id'], + 'original_source' => $metaFieldData['original_source'], + 'recurrence_id' => $metaFieldData['recurrence_id'], + 'bunq_payment_id' => $metaFieldData['bunq_payment_id'], + 'import_hash_v2' => $metaFieldData['import_hash_v2'], + + 'sepa_cc' => $metaFieldData['sepa_cc'], + 'sepa_ct_op' => $metaFieldData['sepa_ct_op'], + 'sepa_ct_id' => $metaFieldData['sepa_ct_id'], + 'sepa_db' => $metaFieldData['sepa_ddb'], + 'sepa_country' => $metaFieldData['sepa_country'], + 'sepa_ep' => $metaFieldData['sepa_ep'], + 'sepa_ci' => $metaFieldData['sepa_ci'], + 'sepa_batch_id' => $metaFieldData['sepa_batch_id'], + + 'interest_date' => $metaDates['interest_date'], + 'book_date' => $metaDates['book_date'], + 'process_date' => $metaDates['process_date'], + 'due_date' => $metaDates['due_date'], + 'payment_date' => $metaDates['payment_date'], + 'invoice_date' => $metaDates['invoice_date'], + ]; + } + /** * @param Collection $transactionJournals * @@ -180,101 +416,7 @@ class TransactionGroupTransformer extends AbstractTransformer $result = []; /** @var TransactionJournal $journal */ foreach ($transactionJournals as $journal) { - $source = $this->getSourceTransaction($journal); - $destination = $this->getDestinationTransaction($journal); - $type = $journal->transactionType->type; - - // get amount - $amount = app('steam')->positive($source->amount); - if (TransactionType::WITHDRAWAL !== $type) { - $amount = app('steam')->negative($source->amount); - } - - // get foreign amount: - $foreignAmount = null; - // @codeCoverageIgnoreStart - if (null !== $source->foreign_amount) { - $foreignAmount = TransactionType::WITHDRAWAL !== $type - ? app('steam')->negative($source->foreign_amount) - : app('steam')->positive($source->foreign_amount); - } - // @codeCoverageIgnoreEnd - - $metaFieldData = $this->groupRepos->getMetaFields($journal->id, $this->metaFields); - $metaDateData = $this->groupRepos->getMetaDateFields($journal->id, $this->metaDateFields); - /** @var Budget $budget */ - $budget = $journal->budgets->first(); - /** @var Category $category */ - $category = $journal->categories->first(); - $currency = $source->transactionCurrency; - $result[] = [ - 'user' => (int)$journal->user_id, - 'transaction_journal_id' => $journal->id, - 'type' => strtolower($type), - 'date' => $journal->date->toAtomString(), - 'order' => $journal->order, - - 'currency_id' => $currency->id, - 'currency_code' => $currency->code, - 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, - - 'foreign_currency_id' => $source->foreignCurrency ? $source->foreignCurrency->id : null, - 'foreign_currency_code' => $source->foreignCurrency ? $source->foreignCurrency->code : null, - 'foreign_currency_symbol' => $source->foreignCurrency ? $source->foreignCurrency->symbol : null, - 'foreign_currency_decimal_places' => $source->foreignCurrency ? $source->foreignCurrency->decimal_places : null, - - 'amount' => $amount, - 'foreign_amount' => $foreignAmount, - - 'description' => $journal->description, - - 'source_id' => $source->account_id, - 'source_name' => $source->account->name, - 'source_iban' => $source->account->iban, - 'source_type' => $source->account->accountType->type, - - 'destination_id' => $destination->account_id, - 'destination_name' => $destination->account->name, - 'destination_iban' => $destination->account->iban, - 'destination_type' => $destination->account->accountType->type, - - 'budget_id' => $budget ? $budget->id : null, - 'budget_name' => $budget ? $budget->name : null, - - 'category_id' => $category ? $category->id : null, - 'category_name' => $category ? $category->name : null, - - 'bill_id' => $journal->bill_id ?: null, - 'bill_name' => $journal->bill ? $journal->bill->name : null, - - 'reconciled' => $source->reconciled, - 'notes' => $this->groupRepos->getNoteText($journal->id), - 'tags' => $this->groupRepos->getTags($journal->id), - - 'internal_reference' => $metaFieldData['internal_reference'], - 'external_id' => $metaFieldData['external_id'], - 'original_source' => $metaFieldData['original_source'], - 'recurrence_id' => $metaFieldData['recurrence_id'], - 'bunq_payment_id' => $metaFieldData['bunq_payment_id'], - 'import_hash_v2' => $metaFieldData['import_hash_v2'], - - 'sepa_cc' => $metaFieldData['sepa_cc'], - 'sepa_ct_op' => $metaFieldData['sepa_ct_op'], - 'sepa_ct_id' => $metaFieldData['sepa_ct_id'], - 'sepa_db' => $metaFieldData['sepa_ddb'], - 'sepa_country' => $metaFieldData['sepa_country'], - 'sepa_ep' => $metaFieldData['sepa_ep'], - 'sepa_ci' => $metaFieldData['sepa_ci'], - 'sepa_batch_id' => $metaFieldData['sepa_batch_id'], - - 'interest_date' => $metaDateData['interest_date'] ? $metaDateData['interest_date']->toAtomString() : null, - 'book_date' => $metaDateData['book_date'] ? $metaDateData['book_date']->toAtomString() : null, - 'process_date' => $metaDateData['process_date'] ? $metaDateData['process_date']->toAtomString() : null, - 'due_date' => $metaDateData['due_date'] ? $metaDateData['due_date']->toAtomString() : null, - 'payment_date' => $metaDateData['payment_date'] ? $metaDateData['payment_date']->toAtomString() : null, - 'invoice_date' => $metaDateData['invoice_date'] ? $metaDateData['invoice_date']->toAtomString() : null, - ]; + $result[] = $this->transformJournal($journal); } return $result; From 9d90beb790d6e2aeb432ce5e460081672d0d6392 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 15 Mar 2020 15:17:07 +0100 Subject: [PATCH 24/53] update libraries for better keyboard use. --- .../V1/Controllers/BudgetLimitController.php | 2 +- .../Controllers/Chart/CategoryController.php | 4 +- app/Http/Requests/Request.php | 2 +- package.json | 13 +- public/v1/js/app.js | 1 + public/v1/js/app.js.LICENSE.txt | 30 + public/v1/js/app_vue.js | 3 +- public/v1/js/app_vue.js.LICENSE.txt | 5 + public/v1/js/create_transaction.js | 3 +- .../v1/js/create_transaction.js.LICENSE.txt | 6 + public/v1/js/edit_transaction.js | 3 +- public/v1/js/edit_transaction.js.LICENSE.txt | 6 + public/v1/js/profile.js | 1 + public/v1/js/profile.js.LICENSE.txt | 6 + .../components/transactions/AccountSelect.vue | 2 +- .../js/components/transactions/Category.vue | 2 +- .../transactions/TransactionDescription.vue | 3 +- yarn.lock | 546 ++++++++---------- 18 files changed, 302 insertions(+), 336 deletions(-) create mode 100644 public/v1/js/app.js.LICENSE.txt create mode 100644 public/v1/js/app_vue.js.LICENSE.txt create mode 100644 public/v1/js/create_transaction.js.LICENSE.txt create mode 100644 public/v1/js/edit_transaction.js.LICENSE.txt create mode 100644 public/v1/js/profile.js.LICENSE.txt diff --git a/app/Api/V1/Controllers/BudgetLimitController.php b/app/Api/V1/Controllers/BudgetLimitController.php index 0a2830253f..14e23e4374 100644 --- a/app/Api/V1/Controllers/BudgetLimitController.php +++ b/app/Api/V1/Controllers/BudgetLimitController.php @@ -165,7 +165,7 @@ class BudgetLimitController extends Controller $data = $request->getAll(); $budget = $this->repository->findNull($data['budget_id']); if (null === $budget) { - throw new FireflyException('200004: Budget does not exist.'); + throw new FireflyException('200004: Budget does not exist.'); // @codeCoverageIgnore } $data['budget'] = $budget; $budgetLimit = $this->blRepository->storeBudgetLimit($data); diff --git a/app/Api/V1/Controllers/Chart/CategoryController.php b/app/Api/V1/Controllers/Chart/CategoryController.php index ce26a0b469..faf4055fe8 100644 --- a/app/Api/V1/Controllers/Chart/CategoryController.php +++ b/app/Api/V1/Controllers/Chart/CategoryController.php @@ -90,8 +90,8 @@ class CategoryController extends Controller $tempData = []; $spentWith = $this->opsRepository->listExpenses($start, $end); $earnedWith = $this->opsRepository->listIncome($start, $end); - $spentWithout = $this->noCatRepository->listExpenses($start, $end); // refactored - $earnedWithout = $this->noCatRepository->listIncome($start, $end); // refactored + $spentWithout = $this->noCatRepository->listExpenses($start, $end); + $earnedWithout = $this->noCatRepository->listIncome($start, $end); $categories = []; diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index b57524fd38..2d9a7385da 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -401,7 +401,7 @@ class Request extends FormRequest if (is_numeric($type)) { $type = (int)$type; } - if (0 === $type || 'none' === $type) { + if (0 === $type || 'none' === $type || '' === $type) { return; } // basic float check: diff --git a/package.json b/package.json index 5b2b0bd7a2..351af832ae 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,11 @@ "bootstrap-sass": "^3.3.7", "cross-env": "^5.1", "font-awesome": "^4.7.0", - "jquery": "^3.1.1", - "laravel-mix": "^4.1.2", - "laravel-mix-bundle-analyzer": "^1.0.5", - "uiv": "^0.31.5", - "vue": "^2.6.10", - "vue-i18n": "^8.14.1", - "vue-template-compiler": "^2.6.10" + "jquery": "^3.4.1", + "laravel-mix": "^5.0", + "uiv": "^0.34", + "vue": "^2.6", + "vue-i18n": "^8.15", + "vue-template-compiler": "^2.6.11" } } diff --git a/public/v1/js/app.js b/public/v1/js/app.js index 3918ec36e0..22e24dfef6 100644 --- a/public/v1/js/app.js +++ b/public/v1/js/app.js @@ -1 +1,2 @@ +/*! For license information please see app.js.LICENSE.txt */ !function(t){var e={};function n(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return t[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(i,o,function(e){return t[e]}.bind(null,o));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="/",n(n.s=72)}({72:function(t,e,n){t.exports=n(73)},73:function(t,e,n){try{window.$=window.jQuery=n(74),n(75)}catch(t){}},74:function(t,e,n){var i;!function(e,n){"use strict";"object"==typeof t.exports?t.exports=e.document?n(e,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return n(t)}:n(e)}("undefined"!=typeof window?window:this,(function(n,o){"use strict";var r=[],s=n.document,a=Object.getPrototypeOf,l=r.slice,u=r.concat,c=r.push,f=r.indexOf,p={},d=p.toString,h=p.hasOwnProperty,g=h.toString,v=g.call(Object),m={},y=function(t){return"function"==typeof t&&"number"!=typeof t.nodeType},b=function(t){return null!=t&&t===t.window},x={type:!0,src:!0,nonce:!0,noModule:!0};function w(t,e,n){var i,o,r=(n=n||s).createElement("script");if(r.text=t,e)for(i in x)(o=e[i]||e.getAttribute&&e.getAttribute(i))&&r.setAttribute(i,o);n.head.appendChild(r).parentNode.removeChild(r)}function T(t){return null==t?t+"":"object"==typeof t||"function"==typeof t?p[d.call(t)]||"object":typeof t}var C=function(t,e){return new C.fn.init(t,e)},E=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function S(t){var e=!!t&&"length"in t&&t.length,n=T(t);return!y(t)&&!b(t)&&("array"===n||0===e||"number"==typeof e&&e>0&&e-1 in t)}C.fn=C.prototype={jquery:"3.4.1",constructor:C,length:0,toArray:function(){return l.call(this)},get:function(t){return null==t?l.call(this):t<0?this[t+this.length]:this[t]},pushStack:function(t){var e=C.merge(this.constructor(),t);return e.prevObject=this,e},each:function(t){return C.each(this,t)},map:function(t){return this.pushStack(C.map(this,(function(e,n){return t.call(e,n,e)})))},slice:function(){return this.pushStack(l.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(t){var e=this.length,n=+t+(t<0?e:0);return this.pushStack(n>=0&&n+~]|"+q+")"+q+"*"),z=new RegExp(q+"|>"),V=new RegExp(F),Q=new RegExp("^"+H+"$"),X={ID:new RegExp("^#("+H+")"),CLASS:new RegExp("^\\.("+H+")"),TAG:new RegExp("^("+H+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+q+"*(even|odd|(([+-]|)(\\d*)n|)"+q+"*(?:([+-]|)"+q+"*(\\d+)|))"+q+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+q+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+q+"*((?:-\\d)?\\d*)"+q+"*\\)|)(?=[^-]|$)","i")},G=/HTML$/i,Y=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,tt=/[+~]/,et=new RegExp("\\\\([\\da-f]{1,6}"+q+"?|("+q+")|.)","ig"),nt=function(t,e,n){var i="0x"+e-65536;return i!=i||n?e:i<0?String.fromCharCode(i+65536):String.fromCharCode(i>>10|55296,1023&i|56320)},it=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ot=function(t,e){return e?"\0"===t?"�":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t},rt=function(){p()},st=xt((function(t){return!0===t.disabled&&"fieldset"===t.nodeName.toLowerCase()}),{dir:"parentNode",next:"legend"});try{I.apply(N=L.call(w.childNodes),w.childNodes),N[w.childNodes.length].nodeType}catch(t){I={apply:N.length?function(t,e){O.apply(t,L.call(e))}:function(t,e){for(var n=t.length,i=0;t[n++]=e[i++];);t.length=n-1}}}function at(t,e,i,o){var r,a,u,c,f,h,m,y=e&&e.ownerDocument,T=e?e.nodeType:9;if(i=i||[],"string"!=typeof t||!t||1!==T&&9!==T&&11!==T)return i;if(!o&&((e?e.ownerDocument||e:w)!==d&&p(e),e=e||d,g)){if(11!==T&&(f=Z.exec(t)))if(r=f[1]){if(9===T){if(!(u=e.getElementById(r)))return i;if(u.id===r)return i.push(u),i}else if(y&&(u=y.getElementById(r))&&b(e,u)&&u.id===r)return i.push(u),i}else{if(f[2])return I.apply(i,e.getElementsByTagName(t)),i;if((r=f[3])&&n.getElementsByClassName&&e.getElementsByClassName)return I.apply(i,e.getElementsByClassName(r)),i}if(n.qsa&&!k[t+" "]&&(!v||!v.test(t))&&(1!==T||"object"!==e.nodeName.toLowerCase())){if(m=t,y=e,1===T&&z.test(t)){for((c=e.getAttribute("id"))?c=c.replace(it,ot):e.setAttribute("id",c=x),a=(h=s(t)).length;a--;)h[a]="#"+c+" "+bt(h[a]);m=h.join(","),y=tt.test(t)&&mt(e.parentNode)||e}try{return I.apply(i,y.querySelectorAll(m)),i}catch(e){k(t,!0)}finally{c===x&&e.removeAttribute("id")}}}return l(t.replace(B,"$1"),e,i,o)}function lt(){var t=[];return function e(n,o){return t.push(n+" ")>i.cacheLength&&delete e[t.shift()],e[n+" "]=o}}function ut(t){return t[x]=!0,t}function ct(t){var e=d.createElement("fieldset");try{return!!t(e)}catch(t){return!1}finally{e.parentNode&&e.parentNode.removeChild(e),e=null}}function ft(t,e){for(var n=t.split("|"),o=n.length;o--;)i.attrHandle[n[o]]=e}function pt(t,e){var n=e&&t,i=n&&1===t.nodeType&&1===e.nodeType&&t.sourceIndex-e.sourceIndex;if(i)return i;if(n)for(;n=n.nextSibling;)if(n===e)return-1;return t?1:-1}function dt(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function ht(t){return function(e){var n=e.nodeName.toLowerCase();return("input"===n||"button"===n)&&e.type===t}}function gt(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&st(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function vt(t){return ut((function(e){return e=+e,ut((function(n,i){for(var o,r=t([],n.length,e),s=r.length;s--;)n[o=r[s]]&&(n[o]=!(i[o]=n[o]))}))}))}function mt(t){return t&&void 0!==t.getElementsByTagName&&t}for(e in n=at.support={},r=at.isXML=function(t){var e=t.namespaceURI,n=(t.ownerDocument||t).documentElement;return!G.test(e||n&&n.nodeName||"HTML")},p=at.setDocument=function(t){var e,o,s=t?t.ownerDocument||t:w;return s!==d&&9===s.nodeType&&s.documentElement?(h=(d=s).documentElement,g=!r(d),w!==d&&(o=d.defaultView)&&o.top!==o&&(o.addEventListener?o.addEventListener("unload",rt,!1):o.attachEvent&&o.attachEvent("onunload",rt)),n.attributes=ct((function(t){return t.className="i",!t.getAttribute("className")})),n.getElementsByTagName=ct((function(t){return t.appendChild(d.createComment("")),!t.getElementsByTagName("*").length})),n.getElementsByClassName=K.test(d.getElementsByClassName),n.getById=ct((function(t){return h.appendChild(t).id=x,!d.getElementsByName||!d.getElementsByName(x).length})),n.getById?(i.filter.ID=function(t){var e=t.replace(et,nt);return function(t){return t.getAttribute("id")===e}},i.find.ID=function(t,e){if(void 0!==e.getElementById&&g){var n=e.getElementById(t);return n?[n]:[]}}):(i.filter.ID=function(t){var e=t.replace(et,nt);return function(t){var n=void 0!==t.getAttributeNode&&t.getAttributeNode("id");return n&&n.value===e}},i.find.ID=function(t,e){if(void 0!==e.getElementById&&g){var n,i,o,r=e.getElementById(t);if(r){if((n=r.getAttributeNode("id"))&&n.value===t)return[r];for(o=e.getElementsByName(t),i=0;r=o[i++];)if((n=r.getAttributeNode("id"))&&n.value===t)return[r]}return[]}}),i.find.TAG=n.getElementsByTagName?function(t,e){return void 0!==e.getElementsByTagName?e.getElementsByTagName(t):n.qsa?e.querySelectorAll(t):void 0}:function(t,e){var n,i=[],o=0,r=e.getElementsByTagName(t);if("*"===t){for(;n=r[o++];)1===n.nodeType&&i.push(n);return i}return r},i.find.CLASS=n.getElementsByClassName&&function(t,e){if(void 0!==e.getElementsByClassName&&g)return e.getElementsByClassName(t)},m=[],v=[],(n.qsa=K.test(d.querySelectorAll))&&(ct((function(t){h.appendChild(t).innerHTML="",t.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+q+"*(?:''|\"\")"),t.querySelectorAll("[selected]").length||v.push("\\["+q+"*(?:value|"+P+")"),t.querySelectorAll("[id~="+x+"-]").length||v.push("~="),t.querySelectorAll(":checked").length||v.push(":checked"),t.querySelectorAll("a#"+x+"+*").length||v.push(".#.+[+~]")})),ct((function(t){t.innerHTML="";var e=d.createElement("input");e.setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),t.querySelectorAll("[name=d]").length&&v.push("name"+q+"*[*^$|!~]?="),2!==t.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),h.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),t.querySelectorAll("*,:x"),v.push(",.*:")}))),(n.matchesSelector=K.test(y=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ct((function(t){n.disconnectedMatch=y.call(t,"*"),y.call(t,"[s!='']:x"),m.push("!=",F)})),v=v.length&&new RegExp(v.join("|")),m=m.length&&new RegExp(m.join("|")),e=K.test(h.compareDocumentPosition),b=e||K.test(h.contains)?function(t,e){var n=9===t.nodeType?t.documentElement:t,i=e&&e.parentNode;return t===i||!(!i||1!==i.nodeType||!(n.contains?n.contains(i):t.compareDocumentPosition&&16&t.compareDocumentPosition(i)))}:function(t,e){if(e)for(;e=e.parentNode;)if(e===t)return!0;return!1},D=e?function(t,e){if(t===e)return f=!0,0;var i=!t.compareDocumentPosition-!e.compareDocumentPosition;return i||(1&(i=(t.ownerDocument||t)===(e.ownerDocument||e)?t.compareDocumentPosition(e):1)||!n.sortDetached&&e.compareDocumentPosition(t)===i?t===d||t.ownerDocument===w&&b(w,t)?-1:e===d||e.ownerDocument===w&&b(w,e)?1:c?R(c,t)-R(c,e):0:4&i?-1:1)}:function(t,e){if(t===e)return f=!0,0;var n,i=0,o=t.parentNode,r=e.parentNode,s=[t],a=[e];if(!o||!r)return t===d?-1:e===d?1:o?-1:r?1:c?R(c,t)-R(c,e):0;if(o===r)return pt(t,e);for(n=t;n=n.parentNode;)s.unshift(n);for(n=e;n=n.parentNode;)a.unshift(n);for(;s[i]===a[i];)i++;return i?pt(s[i],a[i]):s[i]===w?-1:a[i]===w?1:0},d):d},at.matches=function(t,e){return at(t,null,null,e)},at.matchesSelector=function(t,e){if((t.ownerDocument||t)!==d&&p(t),n.matchesSelector&&g&&!k[e+" "]&&(!m||!m.test(e))&&(!v||!v.test(e)))try{var i=y.call(t,e);if(i||n.disconnectedMatch||t.document&&11!==t.document.nodeType)return i}catch(t){k(e,!0)}return at(e,d,null,[t]).length>0},at.contains=function(t,e){return(t.ownerDocument||t)!==d&&p(t),b(t,e)},at.attr=function(t,e){(t.ownerDocument||t)!==d&&p(t);var o=i.attrHandle[e.toLowerCase()],r=o&&A.call(i.attrHandle,e.toLowerCase())?o(t,e,!g):void 0;return void 0!==r?r:n.attributes||!g?t.getAttribute(e):(r=t.getAttributeNode(e))&&r.specified?r.value:null},at.escape=function(t){return(t+"").replace(it,ot)},at.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},at.uniqueSort=function(t){var e,i=[],o=0,r=0;if(f=!n.detectDuplicates,c=!n.sortStable&&t.slice(0),t.sort(D),f){for(;e=t[r++];)e===t[r]&&(o=i.push(r));for(;o--;)t.splice(i[o],1)}return c=null,t},o=at.getText=function(t){var e,n="",i=0,r=t.nodeType;if(r){if(1===r||9===r||11===r){if("string"==typeof t.textContent)return t.textContent;for(t=t.firstChild;t;t=t.nextSibling)n+=o(t)}else if(3===r||4===r)return t.nodeValue}else for(;e=t[i++];)n+=o(e);return n},(i=at.selectors={cacheLength:50,createPseudo:ut,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(et,nt),t[3]=(t[3]||t[4]||t[5]||"").replace(et,nt),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||at.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&at.error(t[0]),t},PSEUDO:function(t){var e,n=!t[6]&&t[2];return X.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":n&&V.test(n)&&(e=s(n,!0))&&(e=n.indexOf(")",n.length-e)-n.length)&&(t[0]=t[0].slice(0,e),t[2]=n.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(et,nt).toLowerCase();return"*"===t?function(){return!0}:function(t){return t.nodeName&&t.nodeName.toLowerCase()===e}},CLASS:function(t){var e=E[t+" "];return e||(e=new RegExp("(^|"+q+")"+t+"("+q+"|$)"))&&E(t,(function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")}))},ATTR:function(t,e,n){return function(i){var o=at.attr(i,t);return null==o?"!="===e:!e||(o+="","="===e?o===n:"!="===e?o!==n:"^="===e?n&&0===o.indexOf(n):"*="===e?n&&o.indexOf(n)>-1:"$="===e?n&&o.slice(-n.length)===n:"~="===e?(" "+o.replace(W," ")+" ").indexOf(n)>-1:"|="===e&&(o===n||o.slice(0,n.length+1)===n+"-"))}},CHILD:function(t,e,n,i,o){var r="nth"!==t.slice(0,3),s="last"!==t.slice(-4),a="of-type"===e;return 1===i&&0===o?function(t){return!!t.parentNode}:function(e,n,l){var u,c,f,p,d,h,g=r!==s?"nextSibling":"previousSibling",v=e.parentNode,m=a&&e.nodeName.toLowerCase(),y=!l&&!a,b=!1;if(v){if(r){for(;g;){for(p=e;p=p[g];)if(a?p.nodeName.toLowerCase()===m:1===p.nodeType)return!1;h=g="only"===t&&!h&&"nextSibling"}return!0}if(h=[s?v.firstChild:v.lastChild],s&&y){for(b=(d=(u=(c=(f=(p=v)[x]||(p[x]={}))[p.uniqueID]||(f[p.uniqueID]={}))[t]||[])[0]===T&&u[1])&&u[2],p=d&&v.childNodes[d];p=++d&&p&&p[g]||(b=d=0)||h.pop();)if(1===p.nodeType&&++b&&p===e){c[t]=[T,d,b];break}}else if(y&&(b=d=(u=(c=(f=(p=e)[x]||(p[x]={}))[p.uniqueID]||(f[p.uniqueID]={}))[t]||[])[0]===T&&u[1]),!1===b)for(;(p=++d&&p&&p[g]||(b=d=0)||h.pop())&&((a?p.nodeName.toLowerCase()!==m:1!==p.nodeType)||!++b||(y&&((c=(f=p[x]||(p[x]={}))[p.uniqueID]||(f[p.uniqueID]={}))[t]=[T,b]),p!==e)););return(b-=o)===i||b%i==0&&b/i>=0}}},PSEUDO:function(t,e){var n,o=i.pseudos[t]||i.setFilters[t.toLowerCase()]||at.error("unsupported pseudo: "+t);return o[x]?o(e):o.length>1?(n=[t,t,"",e],i.setFilters.hasOwnProperty(t.toLowerCase())?ut((function(t,n){for(var i,r=o(t,e),s=r.length;s--;)t[i=R(t,r[s])]=!(n[i]=r[s])})):function(t){return o(t,0,n)}):o}},pseudos:{not:ut((function(t){var e=[],n=[],i=a(t.replace(B,"$1"));return i[x]?ut((function(t,e,n,o){for(var r,s=i(t,null,o,[]),a=t.length;a--;)(r=s[a])&&(t[a]=!(e[a]=r))})):function(t,o,r){return e[0]=t,i(e,null,r,n),e[0]=null,!n.pop()}})),has:ut((function(t){return function(e){return at(t,e).length>0}})),contains:ut((function(t){return t=t.replace(et,nt),function(e){return(e.textContent||o(e)).indexOf(t)>-1}})),lang:ut((function(t){return Q.test(t||"")||at.error("unsupported lang: "+t),t=t.replace(et,nt).toLowerCase(),function(e){var n;do{if(n=g?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(n=n.toLowerCase())===t||0===n.indexOf(t+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}})),target:function(e){var n=t.location&&t.location.hash;return n&&n.slice(1)===e.id},root:function(t){return t===h},focus:function(t){return t===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(t.type||t.href||~t.tabIndex)},enabled:gt(!1),disabled:gt(!0),checked:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&!!t.checked||"option"===e&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!i.pseudos.empty(t)},header:function(t){return J.test(t.nodeName)},input:function(t){return Y.test(t.nodeName)},button:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&"button"===t.type||"button"===e},text:function(t){var e;return"input"===t.nodeName.toLowerCase()&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:vt((function(){return[0]})),last:vt((function(t,e){return[e-1]})),eq:vt((function(t,e,n){return[n<0?n+e:n]})),even:vt((function(t,e){for(var n=0;ne?e:n;--i>=0;)t.push(i);return t})),gt:vt((function(t,e,n){for(var i=n<0?n+e:n;++i1?function(e,n,i){for(var o=t.length;o--;)if(!t[o](e,n,i))return!1;return!0}:t[0]}function Tt(t,e,n,i,o){for(var r,s=[],a=0,l=t.length,u=null!=e;a-1&&(r[u]=!(s[u]=f))}}else m=Tt(m===s?m.splice(h,m.length):m),o?o(null,s,m,l):I.apply(s,m)}))}function Et(t){for(var e,n,o,r=t.length,s=i.relative[t[0].type],a=s||i.relative[" "],l=s?1:0,c=xt((function(t){return t===e}),a,!0),f=xt((function(t){return R(e,t)>-1}),a,!0),p=[function(t,n,i){var o=!s&&(i||n!==u)||((e=n).nodeType?c(t,n,i):f(t,n,i));return e=null,o}];l1&&wt(p),l>1&&bt(t.slice(0,l-1).concat({value:" "===t[l-2].type?"*":""})).replace(B,"$1"),n,l0,o=t.length>0,r=function(r,s,a,l,c){var f,h,v,m=0,y="0",b=r&&[],x=[],w=u,C=r||o&&i.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,S=C.length;for(c&&(u=s===d||s||c);y!==S&&null!=(f=C[y]);y++){if(o&&f){for(h=0,s||f.ownerDocument===d||(p(f),a=!g);v=t[h++];)if(v(f,s||d,a)){l.push(f);break}c&&(T=E)}n&&((f=!v&&f)&&m--,r&&b.push(f))}if(m+=y,n&&y!==m){for(h=0;v=e[h++];)v(b,x,s,a);if(r){if(m>0)for(;y--;)b[y]||x[y]||(x[y]=j.call(l));x=Tt(x)}I.apply(l,x),c&&!r&&x.length>0&&m+e.length>1&&at.uniqueSort(l)}return c&&(T=E,u=w),b};return n?ut(r):r}(r,o))).selector=t}return a},l=at.select=function(t,e,n,o){var r,l,u,c,f,p="function"==typeof t&&t,d=!o&&s(t=p.selector||t);if(n=n||[],1===d.length){if((l=d[0]=d[0].slice(0)).length>2&&"ID"===(u=l[0]).type&&9===e.nodeType&&g&&i.relative[l[1].type]){if(!(e=(i.find.ID(u.matches[0].replace(et,nt),e)||[])[0]))return n;p&&(e=e.parentNode),t=t.slice(l.shift().value.length)}for(r=X.needsContext.test(t)?0:l.length;r--&&(u=l[r],!i.relative[c=u.type]);)if((f=i.find[c])&&(o=f(u.matches[0].replace(et,nt),tt.test(l[0].type)&&mt(e.parentNode)||e))){if(l.splice(r,1),!(t=o.length&&bt(l)))return I.apply(n,o),n;break}}return(p||a(t,d))(o,e,!g,n,!e||tt.test(t)&&mt(e.parentNode)||e),n},n.sortStable=x.split("").sort(D).join("")===x,n.detectDuplicates=!!f,p(),n.sortDetached=ct((function(t){return 1&t.compareDocumentPosition(d.createElement("fieldset"))})),ct((function(t){return t.innerHTML="","#"===t.firstChild.getAttribute("href")}))||ft("type|href|height|width",(function(t,e,n){if(!n)return t.getAttribute(e,"type"===e.toLowerCase()?1:2)})),n.attributes&&ct((function(t){return t.innerHTML="",t.firstChild.setAttribute("value",""),""===t.firstChild.getAttribute("value")}))||ft("value",(function(t,e,n){if(!n&&"input"===t.nodeName.toLowerCase())return t.defaultValue})),ct((function(t){return null==t.getAttribute("disabled")}))||ft(P,(function(t,e,n){var i;if(!n)return!0===t[e]?e.toLowerCase():(i=t.getAttributeNode(e))&&i.specified?i.value:null})),at}(n);C.find=$,C.expr=$.selectors,C.expr[":"]=C.expr.pseudos,C.uniqueSort=C.unique=$.uniqueSort,C.text=$.getText,C.isXMLDoc=$.isXML,C.contains=$.contains,C.escapeSelector=$.escape;var k=function(t,e,n){for(var i=[],o=void 0!==n;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(o&&C(t).is(n))break;i.push(t)}return i},D=function(t,e){for(var n=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&n.push(t);return n},A=C.expr.match.needsContext;function N(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()}var j=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function O(t,e,n){return y(e)?C.grep(t,(function(t,i){return!!e.call(t,i,t)!==n})):e.nodeType?C.grep(t,(function(t){return t===e!==n})):"string"!=typeof e?C.grep(t,(function(t){return f.call(e,t)>-1!==n})):C.filter(e,t,n)}C.filter=function(t,e,n){var i=e[0];return n&&(t=":not("+t+")"),1===e.length&&1===i.nodeType?C.find.matchesSelector(i,t)?[i]:[]:C.find.matches(t,C.grep(e,(function(t){return 1===t.nodeType})))},C.fn.extend({find:function(t){var e,n,i=this.length,o=this;if("string"!=typeof t)return this.pushStack(C(t).filter((function(){for(e=0;e1?C.uniqueSort(n):n},filter:function(t){return this.pushStack(O(this,t||[],!1))},not:function(t){return this.pushStack(O(this,t||[],!0))},is:function(t){return!!O(this,"string"==typeof t&&A.test(t)?C(t):t||[],!1).length}});var I,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(C.fn.init=function(t,e,n){var i,o;if(!t)return this;if(n=n||I,"string"==typeof t){if(!(i="<"===t[0]&&">"===t[t.length-1]&&t.length>=3?[null,t,null]:L.exec(t))||!i[1]&&e)return!e||e.jquery?(e||n).find(t):this.constructor(e).find(t);if(i[1]){if(e=e instanceof C?e[0]:e,C.merge(this,C.parseHTML(i[1],e&&e.nodeType?e.ownerDocument||e:s,!0)),j.test(i[1])&&C.isPlainObject(e))for(i in e)y(this[i])?this[i](e[i]):this.attr(i,e[i]);return this}return(o=s.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return t.nodeType?(this[0]=t,this.length=1,this):y(t)?void 0!==n.ready?n.ready(t):t(C):C.makeArray(t,this)}).prototype=C.fn,I=C(s);var R=/^(?:parents|prev(?:Until|All))/,P={children:!0,contents:!0,next:!0,prev:!0};function q(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}C.fn.extend({has:function(t){var e=C(t,this),n=e.length;return this.filter((function(){for(var t=0;t-1:1===n.nodeType&&C.find.matchesSelector(n,t))){r.push(n);break}return this.pushStack(r.length>1?C.uniqueSort(r):r)},index:function(t){return t?"string"==typeof t?f.call(C(t),this[0]):f.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(C.uniqueSort(C.merge(this.get(),C(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),C.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return k(t,"parentNode")},parentsUntil:function(t,e,n){return k(t,"parentNode",n)},next:function(t){return q(t,"nextSibling")},prev:function(t){return q(t,"previousSibling")},nextAll:function(t){return k(t,"nextSibling")},prevAll:function(t){return k(t,"previousSibling")},nextUntil:function(t,e,n){return k(t,"nextSibling",n)},prevUntil:function(t,e,n){return k(t,"previousSibling",n)},siblings:function(t){return D((t.parentNode||{}).firstChild,t)},children:function(t){return D(t.firstChild)},contents:function(t){return void 0!==t.contentDocument?t.contentDocument:(N(t,"template")&&(t=t.content||t),C.merge([],t.childNodes))}},(function(t,e){C.fn[t]=function(n,i){var o=C.map(this,e,n);return"Until"!==t.slice(-5)&&(i=n),i&&"string"==typeof i&&(o=C.filter(i,o)),this.length>1&&(P[t]||C.uniqueSort(o),R.test(t)&&o.reverse()),this.pushStack(o)}}));var H=/[^\x20\t\r\n\f]+/g;function M(t){return t}function F(t){throw t}function W(t,e,n,i){var o;try{t&&y(o=t.promise)?o.call(t).done(e).fail(n):t&&y(o=t.then)?o.call(t,e,n):e.apply(void 0,[t].slice(i))}catch(t){n.apply(void 0,[t])}}C.Callbacks=function(t){t="string"==typeof t?function(t){var e={};return C.each(t.match(H)||[],(function(t,n){e[n]=!0})),e}(t):C.extend({},t);var e,n,i,o,r=[],s=[],a=-1,l=function(){for(o=o||t.once,i=e=!0;s.length;a=-1)for(n=s.shift();++a-1;)r.splice(n,1),n<=a&&a--})),this},has:function(t){return t?C.inArray(t,r)>-1:r.length>0},empty:function(){return r&&(r=[]),this},disable:function(){return o=s=[],r=n="",this},disabled:function(){return!r},lock:function(){return o=s=[],n||e||(r=n=""),this},locked:function(){return!!o},fireWith:function(t,n){return o||(n=[t,(n=n||[]).slice?n.slice():n],s.push(n),e||l()),this},fire:function(){return u.fireWith(this,arguments),this},fired:function(){return!!i}};return u},C.extend({Deferred:function(t){var e=[["notify","progress",C.Callbacks("memory"),C.Callbacks("memory"),2],["resolve","done",C.Callbacks("once memory"),C.Callbacks("once memory"),0,"resolved"],["reject","fail",C.Callbacks("once memory"),C.Callbacks("once memory"),1,"rejected"]],i="pending",o={state:function(){return i},always:function(){return r.done(arguments).fail(arguments),this},catch:function(t){return o.then(null,t)},pipe:function(){var t=arguments;return C.Deferred((function(n){C.each(e,(function(e,i){var o=y(t[i[4]])&&t[i[4]];r[i[1]]((function(){var t=o&&o.apply(this,arguments);t&&y(t.promise)?t.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[i[0]+"With"](this,o?[t]:arguments)}))})),t=null})).promise()},then:function(t,i,o){var r=0;function s(t,e,i,o){return function(){var a=this,l=arguments,u=function(){var n,u;if(!(t=r&&(i!==F&&(a=void 0,l=[n]),e.rejectWith(a,l))}};t?c():(C.Deferred.getStackHook&&(c.stackTrace=C.Deferred.getStackHook()),n.setTimeout(c))}}return C.Deferred((function(n){e[0][3].add(s(0,n,y(o)?o:M,n.notifyWith)),e[1][3].add(s(0,n,y(t)?t:M)),e[2][3].add(s(0,n,y(i)?i:F))})).promise()},promise:function(t){return null!=t?C.extend(t,o):o}},r={};return C.each(e,(function(t,n){var s=n[2],a=n[5];o[n[1]]=s.add,a&&s.add((function(){i=a}),e[3-t][2].disable,e[3-t][3].disable,e[0][2].lock,e[0][3].lock),s.add(n[3].fire),r[n[0]]=function(){return r[n[0]+"With"](this===r?void 0:this,arguments),this},r[n[0]+"With"]=s.fireWith})),o.promise(r),t&&t.call(r,r),r},when:function(t){var e=arguments.length,n=e,i=Array(n),o=l.call(arguments),r=C.Deferred(),s=function(t){return function(n){i[t]=this,o[t]=arguments.length>1?l.call(arguments):n,--e||r.resolveWith(i,o)}};if(e<=1&&(W(t,r.done(s(n)).resolve,r.reject,!e),"pending"===r.state()||y(o[n]&&o[n].then)))return r.then();for(;n--;)W(o[n],s(n),r.reject);return r.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;C.Deferred.exceptionHook=function(t,e){n.console&&n.console.warn&&t&&B.test(t.name)&&n.console.warn("jQuery.Deferred exception: "+t.message,t.stack,e)},C.readyException=function(t){n.setTimeout((function(){throw t}))};var _=C.Deferred();function U(){s.removeEventListener("DOMContentLoaded",U),n.removeEventListener("load",U),C.ready()}C.fn.ready=function(t){return _.then(t).catch((function(t){C.readyException(t)})),this},C.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--C.readyWait:C.isReady)||(C.isReady=!0,!0!==t&&--C.readyWait>0||_.resolveWith(s,[C]))}}),C.ready.then=_.then,"complete"===s.readyState||"loading"!==s.readyState&&!s.documentElement.doScroll?n.setTimeout(C.ready):(s.addEventListener("DOMContentLoaded",U),n.addEventListener("load",U));var z=function(t,e,n,i,o,r,s){var a=0,l=t.length,u=null==n;if("object"===T(n))for(a in o=!0,n)z(t,e,a,n[a],!0,r,s);else if(void 0!==i&&(o=!0,y(i)||(s=!0),u&&(s?(e.call(t,i),e=null):(u=e,e=function(t,e,n){return u.call(C(t),n)})),e))for(;a1,null,!0)},removeData:function(t){return this.each((function(){Z.remove(this,t)}))}}),C.extend({queue:function(t,e,n){var i;if(t)return e=(e||"fx")+"queue",i=K.get(t,e),n&&(!i||Array.isArray(n)?i=K.access(t,e,C.makeArray(n)):i.push(n)),i||[]},dequeue:function(t,e){e=e||"fx";var n=C.queue(t,e),i=n.length,o=n.shift(),r=C._queueHooks(t,e);"inprogress"===o&&(o=n.shift(),i--),o&&("fx"===e&&n.unshift("inprogress"),delete r.stop,o.call(t,(function(){C.dequeue(t,e)}),r)),!i&&r&&r.empty.fire()},_queueHooks:function(t,e){var n=e+"queueHooks";return K.get(t,n)||K.access(t,n,{empty:C.Callbacks("once memory").add((function(){K.remove(t,[e+"queue",n])}))})}}),C.fn.extend({queue:function(t,e){var n=2;return"string"!=typeof t&&(e=t,t="fx",n--),arguments.length\x20\t\r\n\f]*)/i,mt=/^$|^module$|\/(?:java|ecma)script/i,yt={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function bt(t,e){var n;return n=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[],void 0===e||e&&N(t,e)?C.merge([t],n):n}function xt(t,e){for(var n=0,i=t.length;n-1)o&&o.push(r);else if(u=at(r),s=bt(f.appendChild(r),"script"),u&&xt(s),n)for(c=0;r=s[c++];)mt.test(r.type||"")&&n.push(r);return f}wt=s.createDocumentFragment().appendChild(s.createElement("div")),(Tt=s.createElement("input")).setAttribute("type","radio"),Tt.setAttribute("checked","checked"),Tt.setAttribute("name","t"),wt.appendChild(Tt),m.checkClone=wt.cloneNode(!0).cloneNode(!0).lastChild.checked,wt.innerHTML="",m.noCloneChecked=!!wt.cloneNode(!0).lastChild.defaultValue;var St=/^key/,$t=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,kt=/^([^.]*)(?:\.(.+)|)/;function Dt(){return!0}function At(){return!1}function Nt(t,e){return t===function(){try{return s.activeElement}catch(t){}}()==("focus"===e)}function jt(t,e,n,i,o,r){var s,a;if("object"==typeof e){for(a in"string"!=typeof n&&(i=i||n,n=void 0),e)jt(t,a,n,i,e[a],r);return t}if(null==i&&null==o?(o=n,i=n=void 0):null==o&&("string"==typeof n?(o=i,i=void 0):(o=i,i=n,n=void 0)),!1===o)o=At;else if(!o)return t;return 1===r&&(s=o,(o=function(t){return C().off(t),s.apply(this,arguments)}).guid=s.guid||(s.guid=C.guid++)),t.each((function(){C.event.add(this,e,o,i,n)}))}function Ot(t,e,n){n?(K.set(t,e,!1),C.event.add(t,e,{namespace:!1,handler:function(t){var i,o,r=K.get(this,e);if(1&t.isTrigger&&this[e]){if(r.length)(C.event.special[e]||{}).delegateType&&t.stopPropagation();else if(r=l.call(arguments),K.set(this,e,r),i=n(this,e),this[e](),r!==(o=K.get(this,e))||i?K.set(this,e,!1):o={},r!==o)return t.stopImmediatePropagation(),t.preventDefault(),o.value}else r.length&&(K.set(this,e,{value:C.event.trigger(C.extend(r[0],C.Event.prototype),r.slice(1),this)}),t.stopImmediatePropagation())}})):void 0===K.get(t,e)&&C.event.add(t,e,Dt)}C.event={global:{},add:function(t,e,n,i,o){var r,s,a,l,u,c,f,p,d,h,g,v=K.get(t);if(v)for(n.handler&&(n=(r=n).handler,o=r.selector),o&&C.find.matchesSelector(st,o),n.guid||(n.guid=C.guid++),(l=v.events)||(l=v.events={}),(s=v.handle)||(s=v.handle=function(e){return void 0!==C&&C.event.triggered!==e.type?C.event.dispatch.apply(t,arguments):void 0}),u=(e=(e||"").match(H)||[""]).length;u--;)d=g=(a=kt.exec(e[u])||[])[1],h=(a[2]||"").split(".").sort(),d&&(f=C.event.special[d]||{},d=(o?f.delegateType:f.bindType)||d,f=C.event.special[d]||{},c=C.extend({type:d,origType:g,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&C.expr.match.needsContext.test(o),namespace:h.join(".")},r),(p=l[d])||((p=l[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,i,h,s)||t.addEventListener&&t.addEventListener(d,s)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),o?p.splice(p.delegateCount++,0,c):p.push(c),C.event.global[d]=!0)},remove:function(t,e,n,i,o){var r,s,a,l,u,c,f,p,d,h,g,v=K.hasData(t)&&K.get(t);if(v&&(l=v.events)){for(u=(e=(e||"").match(H)||[""]).length;u--;)if(d=g=(a=kt.exec(e[u])||[])[1],h=(a[2]||"").split(".").sort(),d){for(f=C.event.special[d]||{},p=l[d=(i?f.delegateType:f.bindType)||d]||[],a=a[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=r=p.length;r--;)c=p[r],!o&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||i&&i!==c.selector&&("**"!==i||!c.selector)||(p.splice(r,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(t,c));s&&!p.length&&(f.teardown&&!1!==f.teardown.call(t,h,v.handle)||C.removeEvent(t,d,v.handle),delete l[d])}else for(d in l)C.event.remove(t,d+e[u],n,i,!0);C.isEmptyObject(l)&&K.remove(t,"handle events")}},dispatch:function(t){var e,n,i,o,r,s,a=C.event.fix(t),l=new Array(arguments.length),u=(K.get(this,"events")||{})[a.type]||[],c=C.event.special[a.type]||{};for(l[0]=a,e=1;e=1))for(;u!==this;u=u.parentNode||this)if(1===u.nodeType&&("click"!==t.type||!0!==u.disabled)){for(r=[],s={},n=0;n-1:C.find(o,this,null,[u]).length),s[o]&&r.push(i);r.length&&a.push({elem:u,handlers:r})}return u=this,l\x20\t\r\n\f]*)[^>]*)\/>/gi,Lt=/\s*$/g;function qt(t,e){return N(t,"table")&&N(11!==e.nodeType?e:e.firstChild,"tr")&&C(t).children("tbody")[0]||t}function Ht(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function Mt(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function Ft(t,e){var n,i,o,r,s,a,l,u;if(1===e.nodeType){if(K.hasData(t)&&(r=K.access(t),s=K.set(e,r),u=r.events))for(o in delete s.handle,s.events={},u)for(n=0,i=u[o].length;n1&&"string"==typeof h&&!m.checkClone&&Rt.test(h))return t.each((function(o){var r=t.eq(o);g&&(e[0]=h.call(this,o,r.html())),Bt(r,e,n,i)}));if(p&&(r=(o=Et(e,t[0].ownerDocument,!1,t,i)).firstChild,1===o.childNodes.length&&(o=r),r||i)){for(a=(s=C.map(bt(o,"script"),Ht)).length;f")},clone:function(t,e,n){var i,o,r,s,a=t.cloneNode(!0),l=at(t);if(!(m.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||C.isXMLDoc(t)))for(s=bt(a),i=0,o=(r=bt(t)).length;i0&&xt(s,!l&&bt(t,"script")),a},cleanData:function(t){for(var e,n,i,o=C.event.special,r=0;void 0!==(n=t[r]);r++)if(Y(n)){if(e=n[K.expando]){if(e.events)for(i in e.events)o[i]?C.event.remove(n,i):C.removeEvent(n,i,e.handle);n[K.expando]=void 0}n[Z.expando]&&(n[Z.expando]=void 0)}}}),C.fn.extend({detach:function(t){return _t(this,t,!0)},remove:function(t){return _t(this,t)},text:function(t){return z(this,(function(t){return void 0===t?C.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)}))}),null,t,arguments.length)},append:function(){return Bt(this,arguments,(function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||qt(this,t).appendChild(t)}))},prepend:function(){return Bt(this,arguments,(function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=qt(this,t);e.insertBefore(t,e.firstChild)}}))},before:function(){return Bt(this,arguments,(function(t){this.parentNode&&this.parentNode.insertBefore(t,this)}))},after:function(){return Bt(this,arguments,(function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)}))},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(C.cleanData(bt(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map((function(){return C.clone(this,t,e)}))},html:function(t){return z(this,(function(t){var e=this[0]||{},n=0,i=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!Lt.test(t)&&!yt[(vt.exec(t)||["",""])[1].toLowerCase()]){t=C.htmlPrefilter(t);try{for(;n=0&&(l+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-r-l-a-.5))||0),l}function re(t,e,n){var i=zt(t),o=(!m.boxSizingReliable()||n)&&"border-box"===C.css(t,"boxSizing",!1,i),r=o,s=Qt(t,e,i),a="offset"+e[0].toUpperCase()+e.slice(1);if(Ut.test(s)){if(!n)return s;s="auto"}return(!m.boxSizingReliable()&&o||"auto"===s||!parseFloat(s)&&"inline"===C.css(t,"display",!1,i))&&t.getClientRects().length&&(o="border-box"===C.css(t,"boxSizing",!1,i),(r=a in t)&&(s=t[a])),(s=parseFloat(s)||0)+oe(t,e,n||(o?"border":"content"),r,i,s)+"px"}function se(t,e,n,i,o){return new se.prototype.init(t,e,n,i,o)}C.extend({cssHooks:{opacity:{get:function(t,e){if(e){var n=Qt(t,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(t,e,n,i){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var o,r,s,a=G(e),l=te.test(e),u=t.style;if(l||(e=Kt(a)),s=C.cssHooks[e]||C.cssHooks[a],void 0===n)return s&&"get"in s&&void 0!==(o=s.get(t,!1,i))?o:u[e];"string"===(r=typeof n)&&(o=ot.exec(n))&&o[1]&&(n=ft(t,e,o),r="number"),null!=n&&n==n&&("number"!==r||l||(n+=o&&o[3]||(C.cssNumber[a]?"":"px")),m.clearCloneStyle||""!==n||0!==e.indexOf("background")||(u[e]="inherit"),s&&"set"in s&&void 0===(n=s.set(t,n,i))||(l?u.setProperty(e,n):u[e]=n))}},css:function(t,e,n,i){var o,r,s,a=G(e);return te.test(e)||(e=Kt(a)),(s=C.cssHooks[e]||C.cssHooks[a])&&"get"in s&&(o=s.get(t,!0,n)),void 0===o&&(o=Qt(t,e,i)),"normal"===o&&e in ne&&(o=ne[e]),""===n||n?(r=parseFloat(o),!0===n||isFinite(r)?r||0:o):o}}),C.each(["height","width"],(function(t,e){C.cssHooks[e]={get:function(t,n,i){if(n)return!Zt.test(C.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?re(t,e,i):ct(t,ee,(function(){return re(t,e,i)}))},set:function(t,n,i){var o,r=zt(t),s=!m.scrollboxSize()&&"absolute"===r.position,a=(s||i)&&"border-box"===C.css(t,"boxSizing",!1,r),l=i?oe(t,e,i,a,r):0;return a&&s&&(l-=Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-parseFloat(r[e])-oe(t,e,"border",!1,r)-.5)),l&&(o=ot.exec(n))&&"px"!==(o[3]||"px")&&(t.style[e]=n,n=C.css(t,e)),ie(0,n,l)}}})),C.cssHooks.marginLeft=Xt(m.reliableMarginLeft,(function(t,e){if(e)return(parseFloat(Qt(t,"marginLeft"))||t.getBoundingClientRect().left-ct(t,{marginLeft:0},(function(){return t.getBoundingClientRect().left})))+"px"})),C.each({margin:"",padding:"",border:"Width"},(function(t,e){C.cssHooks[t+e]={expand:function(n){for(var i=0,o={},r="string"==typeof n?n.split(" "):[n];i<4;i++)o[t+rt[i]+e]=r[i]||r[i-2]||r[0];return o}},"margin"!==t&&(C.cssHooks[t+e].set=ie)})),C.fn.extend({css:function(t,e){return z(this,(function(t,e,n){var i,o,r={},s=0;if(Array.isArray(e)){for(i=zt(t),o=e.length;s1)}}),C.Tween=se,se.prototype={constructor:se,init:function(t,e,n,i,o,r){this.elem=t,this.prop=n,this.easing=o||C.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=i,this.unit=r||(C.cssNumber[n]?"":"px")},cur:function(){var t=se.propHooks[this.prop];return t&&t.get?t.get(this):se.propHooks._default.get(this)},run:function(t){var e,n=se.propHooks[this.prop];return this.options.duration?this.pos=e=C.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):se.propHooks._default.set(this),this}},se.prototype.init.prototype=se.prototype,se.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=C.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){C.fx.step[t.prop]?C.fx.step[t.prop](t):1!==t.elem.nodeType||!C.cssHooks[t.prop]&&null==t.elem.style[Kt(t.prop)]?t.elem[t.prop]=t.now:C.style(t.elem,t.prop,t.now+t.unit)}}},se.propHooks.scrollTop=se.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},C.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},C.fx=se.prototype.init,C.fx.step={};var ae,le,ue=/^(?:toggle|show|hide)$/,ce=/queueHooks$/;function fe(){le&&(!1===s.hidden&&n.requestAnimationFrame?n.requestAnimationFrame(fe):n.setTimeout(fe,C.fx.interval),C.fx.tick())}function pe(){return n.setTimeout((function(){ae=void 0})),ae=Date.now()}function de(t,e){var n,i=0,o={height:t};for(e=e?1:0;i<4;i+=2-e)o["margin"+(n=rt[i])]=o["padding"+n]=t;return e&&(o.opacity=o.width=t),o}function he(t,e,n){for(var i,o=(ge.tweeners[e]||[]).concat(ge.tweeners["*"]),r=0,s=o.length;r1)},removeAttr:function(t){return this.each((function(){C.removeAttr(this,t)}))}}),C.extend({attr:function(t,e,n){var i,o,r=t.nodeType;if(3!==r&&8!==r&&2!==r)return void 0===t.getAttribute?C.prop(t,e,n):(1===r&&C.isXMLDoc(t)||(o=C.attrHooks[e.toLowerCase()]||(C.expr.match.bool.test(e)?ve:void 0)),void 0!==n?null===n?void C.removeAttr(t,e):o&&"set"in o&&void 0!==(i=o.set(t,n,e))?i:(t.setAttribute(e,n+""),n):o&&"get"in o&&null!==(i=o.get(t,e))?i:null==(i=C.find.attr(t,e))?void 0:i)},attrHooks:{type:{set:function(t,e){if(!m.radioValue&&"radio"===e&&N(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,i=0,o=e&&e.match(H);if(o&&1===t.nodeType)for(;n=o[i++];)t.removeAttribute(n)}}),ve={set:function(t,e,n){return!1===e?C.removeAttr(t,n):t.setAttribute(n,n),n}},C.each(C.expr.match.bool.source.match(/\w+/g),(function(t,e){var n=me[e]||C.find.attr;me[e]=function(t,e,i){var o,r,s=e.toLowerCase();return i||(r=me[s],me[s]=o,o=null!=n(t,e,i)?s:null,me[s]=r),o}}));var ye=/^(?:input|select|textarea|button)$/i,be=/^(?:a|area)$/i;function xe(t){return(t.match(H)||[]).join(" ")}function we(t){return t.getAttribute&&t.getAttribute("class")||""}function Te(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(H)||[]}C.fn.extend({prop:function(t,e){return z(this,C.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each((function(){delete this[C.propFix[t]||t]}))}}),C.extend({prop:function(t,e,n){var i,o,r=t.nodeType;if(3!==r&&8!==r&&2!==r)return 1===r&&C.isXMLDoc(t)||(e=C.propFix[e]||e,o=C.propHooks[e]),void 0!==n?o&&"set"in o&&void 0!==(i=o.set(t,n,e))?i:t[e]=n:o&&"get"in o&&null!==(i=o.get(t,e))?i:t[e]},propHooks:{tabIndex:{get:function(t){var e=C.find.attr(t,"tabindex");return e?parseInt(e,10):ye.test(t.nodeName)||be.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),m.optSelected||(C.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),C.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],(function(){C.propFix[this.toLowerCase()]=this})),C.fn.extend({addClass:function(t){var e,n,i,o,r,s,a,l=0;if(y(t))return this.each((function(e){C(this).addClass(t.call(this,e,we(this)))}));if((e=Te(t)).length)for(;n=this[l++];)if(o=we(n),i=1===n.nodeType&&" "+xe(o)+" "){for(s=0;r=e[s++];)i.indexOf(" "+r+" ")<0&&(i+=r+" ");o!==(a=xe(i))&&n.setAttribute("class",a)}return this},removeClass:function(t){var e,n,i,o,r,s,a,l=0;if(y(t))return this.each((function(e){C(this).removeClass(t.call(this,e,we(this)))}));if(!arguments.length)return this.attr("class","");if((e=Te(t)).length)for(;n=this[l++];)if(o=we(n),i=1===n.nodeType&&" "+xe(o)+" "){for(s=0;r=e[s++];)for(;i.indexOf(" "+r+" ")>-1;)i=i.replace(" "+r+" "," ");o!==(a=xe(i))&&n.setAttribute("class",a)}return this},toggleClass:function(t,e){var n=typeof t,i="string"===n||Array.isArray(t);return"boolean"==typeof e&&i?e?this.addClass(t):this.removeClass(t):y(t)?this.each((function(n){C(this).toggleClass(t.call(this,n,we(this),e),e)})):this.each((function(){var e,o,r,s;if(i)for(o=0,r=C(this),s=Te(t);e=s[o++];)r.hasClass(e)?r.removeClass(e):r.addClass(e);else void 0!==t&&"boolean"!==n||((e=we(this))&&K.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":K.get(this,"__className__")||""))}))},hasClass:function(t){var e,n,i=0;for(e=" "+t+" ";n=this[i++];)if(1===n.nodeType&&(" "+xe(we(n))+" ").indexOf(e)>-1)return!0;return!1}});var Ce=/\r/g;C.fn.extend({val:function(t){var e,n,i,o=this[0];return arguments.length?(i=y(t),this.each((function(n){var o;1===this.nodeType&&(null==(o=i?t.call(this,n,C(this).val()):t)?o="":"number"==typeof o?o+="":Array.isArray(o)&&(o=C.map(o,(function(t){return null==t?"":t+""}))),(e=C.valHooks[this.type]||C.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,o,"value")||(this.value=o))}))):o?(e=C.valHooks[o.type]||C.valHooks[o.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(o,"value"))?n:"string"==typeof(n=o.value)?n.replace(Ce,""):null==n?"":n:void 0}}),C.extend({valHooks:{option:{get:function(t){var e=C.find.attr(t,"value");return null!=e?e:xe(C.text(t))}},select:{get:function(t){var e,n,i,o=t.options,r=t.selectedIndex,s="select-one"===t.type,a=s?null:[],l=s?r+1:o.length;for(i=r<0?l:s?r:0;i-1)&&(n=!0);return n||(t.selectedIndex=-1),r}}}}),C.each(["radio","checkbox"],(function(){C.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=C.inArray(C(t).val(),e)>-1}},m.checkOn||(C.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})})),m.focusin="onfocusin"in n;var Ee=/^(?:focusinfocus|focusoutblur)$/,Se=function(t){t.stopPropagation()};C.extend(C.event,{trigger:function(t,e,i,o){var r,a,l,u,c,f,p,d,g=[i||s],v=h.call(t,"type")?t.type:t,m=h.call(t,"namespace")?t.namespace.split("."):[];if(a=d=l=i=i||s,3!==i.nodeType&&8!==i.nodeType&&!Ee.test(v+C.event.triggered)&&(v.indexOf(".")>-1&&(m=v.split("."),v=m.shift(),m.sort()),c=v.indexOf(":")<0&&"on"+v,(t=t[C.expando]?t:new C.Event(v,"object"==typeof t&&t)).isTrigger=o?2:3,t.namespace=m.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),e=null==e?[t]:C.makeArray(e,[t]),p=C.event.special[v]||{},o||!p.trigger||!1!==p.trigger.apply(i,e))){if(!o&&!p.noBubble&&!b(i)){for(u=p.delegateType||v,Ee.test(u+v)||(a=a.parentNode);a;a=a.parentNode)g.push(a),l=a;l===(i.ownerDocument||s)&&g.push(l.defaultView||l.parentWindow||n)}for(r=0;(a=g[r++])&&!t.isPropagationStopped();)d=a,t.type=r>1?u:p.bindType||v,(f=(K.get(a,"events")||{})[t.type]&&K.get(a,"handle"))&&f.apply(a,e),(f=c&&a[c])&&f.apply&&Y(a)&&(t.result=f.apply(a,e),!1===t.result&&t.preventDefault());return t.type=v,o||t.isDefaultPrevented()||p._default&&!1!==p._default.apply(g.pop(),e)||!Y(i)||c&&y(i[v])&&!b(i)&&((l=i[c])&&(i[c]=null),C.event.triggered=v,t.isPropagationStopped()&&d.addEventListener(v,Se),i[v](),t.isPropagationStopped()&&d.removeEventListener(v,Se),C.event.triggered=void 0,l&&(i[c]=l)),t.result}},simulate:function(t,e,n){var i=C.extend(new C.Event,n,{type:t,isSimulated:!0});C.event.trigger(i,null,e)}}),C.fn.extend({trigger:function(t,e){return this.each((function(){C.event.trigger(t,e,this)}))},triggerHandler:function(t,e){var n=this[0];if(n)return C.event.trigger(t,e,n,!0)}}),m.focusin||C.each({focus:"focusin",blur:"focusout"},(function(t,e){var n=function(t){C.event.simulate(e,t.target,C.event.fix(t))};C.event.special[e]={setup:function(){var i=this.ownerDocument||this,o=K.access(i,e);o||i.addEventListener(t,n,!0),K.access(i,e,(o||0)+1)},teardown:function(){var i=this.ownerDocument||this,o=K.access(i,e)-1;o?K.access(i,e,o):(i.removeEventListener(t,n,!0),K.remove(i,e))}}}));var $e=n.location,ke=Date.now(),De=/\?/;C.parseXML=function(t){var e;if(!t||"string"!=typeof t)return null;try{e=(new n.DOMParser).parseFromString(t,"text/xml")}catch(t){e=void 0}return e&&!e.getElementsByTagName("parsererror").length||C.error("Invalid XML: "+t),e};var Ae=/\[\]$/,Ne=/\r?\n/g,je=/^(?:submit|button|image|reset|file)$/i,Oe=/^(?:input|select|textarea|keygen)/i;function Ie(t,e,n,i){var o;if(Array.isArray(e))C.each(e,(function(e,o){n||Ae.test(t)?i(t,o):Ie(t+"["+("object"==typeof o&&null!=o?e:"")+"]",o,n,i)}));else if(n||"object"!==T(e))i(t,e);else for(o in e)Ie(t+"["+o+"]",e[o],n,i)}C.param=function(t,e){var n,i=[],o=function(t,e){var n=y(e)?e():e;i[i.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(null==t)return"";if(Array.isArray(t)||t.jquery&&!C.isPlainObject(t))C.each(t,(function(){o(this.name,this.value)}));else for(n in t)Ie(n,t[n],e,o);return i.join("&")},C.fn.extend({serialize:function(){return C.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var t=C.prop(this,"elements");return t?C.makeArray(t):this})).filter((function(){var t=this.type;return this.name&&!C(this).is(":disabled")&&Oe.test(this.nodeName)&&!je.test(t)&&(this.checked||!gt.test(t))})).map((function(t,e){var n=C(this).val();return null==n?null:Array.isArray(n)?C.map(n,(function(t){return{name:e.name,value:t.replace(Ne,"\r\n")}})):{name:e.name,value:n.replace(Ne,"\r\n")}})).get()}});var Le=/%20/g,Re=/#.*$/,Pe=/([?&])_=[^&]*/,qe=/^(.*?):[ \t]*([^\r\n]*)$/gm,He=/^(?:GET|HEAD)$/,Me=/^\/\//,Fe={},We={},Be="*/".concat("*"),_e=s.createElement("a");function Ue(t){return function(e,n){"string"!=typeof e&&(n=e,e="*");var i,o=0,r=e.toLowerCase().match(H)||[];if(y(n))for(;i=r[o++];)"+"===i[0]?(i=i.slice(1)||"*",(t[i]=t[i]||[]).unshift(n)):(t[i]=t[i]||[]).push(n)}}function ze(t,e,n,i){var o={},r=t===We;function s(a){var l;return o[a]=!0,C.each(t[a]||[],(function(t,a){var u=a(e,n,i);return"string"!=typeof u||r||o[u]?r?!(l=u):void 0:(e.dataTypes.unshift(u),s(u),!1)})),l}return s(e.dataTypes[0])||!o["*"]&&s("*")}function Ve(t,e){var n,i,o=C.ajaxSettings.flatOptions||{};for(n in e)void 0!==e[n]&&((o[n]?t:i||(i={}))[n]=e[n]);return i&&C.extend(!0,t,i),t}_e.href=$e.href,C.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:$e.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test($e.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Be,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":C.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Ve(Ve(t,C.ajaxSettings),e):Ve(C.ajaxSettings,t)},ajaxPrefilter:Ue(Fe),ajaxTransport:Ue(We),ajax:function(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var i,o,r,a,l,u,c,f,p,d,h=C.ajaxSetup({},e),g=h.context||h,v=h.context&&(g.nodeType||g.jquery)?C(g):C.event,m=C.Deferred(),y=C.Callbacks("once memory"),b=h.statusCode||{},x={},w={},T="canceled",E={readyState:0,getResponseHeader:function(t){var e;if(c){if(!a)for(a={};e=qe.exec(r);)a[e[1].toLowerCase()+" "]=(a[e[1].toLowerCase()+" "]||[]).concat(e[2]);e=a[t.toLowerCase()+" "]}return null==e?null:e.join(", ")},getAllResponseHeaders:function(){return c?r:null},setRequestHeader:function(t,e){return null==c&&(t=w[t.toLowerCase()]=w[t.toLowerCase()]||t,x[t]=e),this},overrideMimeType:function(t){return null==c&&(h.mimeType=t),this},statusCode:function(t){var e;if(t)if(c)E.always(t[E.status]);else for(e in t)b[e]=[b[e],t[e]];return this},abort:function(t){var e=t||T;return i&&i.abort(e),S(0,e),this}};if(m.promise(E),h.url=((t||h.url||$e.href)+"").replace(Me,$e.protocol+"//"),h.type=e.method||e.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(H)||[""],null==h.crossDomain){u=s.createElement("a");try{u.href=h.url,u.href=u.href,h.crossDomain=_e.protocol+"//"+_e.host!=u.protocol+"//"+u.host}catch(t){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=C.param(h.data,h.traditional)),ze(Fe,h,e,E),c)return E;for(p in(f=C.event&&h.global)&&0==C.active++&&C.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!He.test(h.type),o=h.url.replace(Re,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(Le,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(De.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Pe,"$1"),d=(De.test(o)?"&":"?")+"_="+ke+++d),h.url=o+d),h.ifModified&&(C.lastModified[o]&&E.setRequestHeader("If-Modified-Since",C.lastModified[o]),C.etag[o]&&E.setRequestHeader("If-None-Match",C.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||e.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+Be+"; q=0.01":""):h.accepts["*"]),h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(T="abort",y.add(h.complete),E.done(h.success),E.fail(h.error),i=ze(We,h,e,E)){if(E.readyState=1,f&&v.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(l=n.setTimeout((function(){E.abort("timeout")}),h.timeout));try{c=!1,i.send(x,S)}catch(t){if(c)throw t;S(-1,t)}}else S(-1,"No Transport");function S(t,e,s,a){var u,p,d,x,w,T=e;c||(c=!0,l&&n.clearTimeout(l),i=void 0,r=a||"",E.readyState=t>0?4:0,u=t>=200&&t<300||304===t,s&&(x=function(t,e,n){for(var i,o,r,s,a=t.contents,l=t.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=t.mimeType||e.getResponseHeader("Content-Type"));if(i)for(o in a)if(a[o]&&a[o].test(i)){l.unshift(o);break}if(l[0]in n)r=l[0];else{for(o in n){if(!l[0]||t.converters[o+" "+l[0]]){r=o;break}s||(s=o)}r=r||s}if(r)return r!==l[0]&&l.unshift(r),n[r]}(h,E,s)),x=function(t,e,n,i){var o,r,s,a,l,u={},c=t.dataTypes.slice();if(c[1])for(s in t.converters)u[s.toLowerCase()]=t.converters[s];for(r=c.shift();r;)if(t.responseFields[r]&&(n[t.responseFields[r]]=e),!l&&i&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),l=r,r=c.shift())if("*"===r)r=l;else if("*"!==l&&l!==r){if(!(s=u[l+" "+r]||u["* "+r]))for(o in u)if((a=o.split(" "))[1]===r&&(s=u[l+" "+a[0]]||u["* "+a[0]])){!0===s?s=u[o]:!0!==u[o]&&(r=a[0],c.unshift(a[1]));break}if(!0!==s)if(s&&t.throws)e=s(e);else try{e=s(e)}catch(t){return{state:"parsererror",error:s?t:"No conversion from "+l+" to "+r}}}return{state:"success",data:e}}(h,x,E,u),u?(h.ifModified&&((w=E.getResponseHeader("Last-Modified"))&&(C.lastModified[o]=w),(w=E.getResponseHeader("etag"))&&(C.etag[o]=w)),204===t||"HEAD"===h.type?T="nocontent":304===t?T="notmodified":(T=x.state,p=x.data,u=!(d=x.error))):(d=T,!t&&T||(T="error",t<0&&(t=0))),E.status=t,E.statusText=(e||T)+"",u?m.resolveWith(g,[p,T,E]):m.rejectWith(g,[E,T,d]),E.statusCode(b),b=void 0,f&&v.trigger(u?"ajaxSuccess":"ajaxError",[E,h,u?p:d]),y.fireWith(g,[E,T]),f&&(v.trigger("ajaxComplete",[E,h]),--C.active||C.event.trigger("ajaxStop")))}return E},getJSON:function(t,e,n){return C.get(t,e,n,"json")},getScript:function(t,e){return C.get(t,void 0,e,"script")}}),C.each(["get","post"],(function(t,e){C[e]=function(t,n,i,o){return y(n)&&(o=o||i,i=n,n=void 0),C.ajax(C.extend({url:t,type:e,dataType:o,data:n,success:i},C.isPlainObject(t)&&t))}})),C._evalUrl=function(t,e){return C.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(t){C.globalEval(t,e)}})},C.fn.extend({wrapAll:function(t){var e;return this[0]&&(y(t)&&(t=t.call(this[0])),e=C(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map((function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t})).append(this)),this},wrapInner:function(t){return y(t)?this.each((function(e){C(this).wrapInner(t.call(this,e))})):this.each((function(){var e=C(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)}))},wrap:function(t){var e=y(t);return this.each((function(n){C(this).wrapAll(e?t.call(this,n):t)}))},unwrap:function(t){return this.parent(t).not("body").each((function(){C(this).replaceWith(this.childNodes)})),this}}),C.expr.pseudos.hidden=function(t){return!C.expr.pseudos.visible(t)},C.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},C.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(t){}};var Qe={0:200,1223:204},Xe=C.ajaxSettings.xhr();m.cors=!!Xe&&"withCredentials"in Xe,m.ajax=Xe=!!Xe,C.ajaxTransport((function(t){var e,i;if(m.cors||Xe&&!t.crossDomain)return{send:function(o,r){var s,a=t.xhr();if(a.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(s in t.xhrFields)a[s]=t.xhrFields[s];for(s in t.mimeType&&a.overrideMimeType&&a.overrideMimeType(t.mimeType),t.crossDomain||o["X-Requested-With"]||(o["X-Requested-With"]="XMLHttpRequest"),o)a.setRequestHeader(s,o[s]);e=function(t){return function(){e&&(e=i=a.onload=a.onerror=a.onabort=a.ontimeout=a.onreadystatechange=null,"abort"===t?a.abort():"error"===t?"number"!=typeof a.status?r(0,"error"):r(a.status,a.statusText):r(Qe[a.status]||a.status,a.statusText,"text"!==(a.responseType||"text")||"string"!=typeof a.responseText?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=e(),i=a.onerror=a.ontimeout=e("error"),void 0!==a.onabort?a.onabort=i:a.onreadystatechange=function(){4===a.readyState&&n.setTimeout((function(){e&&i()}))},e=e("abort");try{a.send(t.hasContent&&t.data||null)}catch(t){if(e)throw t}},abort:function(){e&&e()}}})),C.ajaxPrefilter((function(t){t.crossDomain&&(t.contents.script=!1)})),C.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return C.globalEval(t),t}}}),C.ajaxPrefilter("script",(function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")})),C.ajaxTransport("script",(function(t){var e,n;if(t.crossDomain||t.scriptAttrs)return{send:function(i,o){e=C("