Merge pull request #7503 from firefly-iii/feat/adjusted-budget

feat: a budget type that will rollover but also incorporate overspend…
This commit is contained in:
James Cole 2023-05-15 06:18:23 +02:00 committed by GitHub
commit cf7c01e5d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 100 additions and 9 deletions

View File

@ -71,6 +71,8 @@ class TransactionController extends Controller
$resource = new Collection($transactions, $transformer, 'transactions');
$resource->setPaginator(new IlluminatePaginatorAdapter($groups));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
$array = $manager->createData($resource)->toArray();
return response()->json($array)->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@ -54,6 +54,10 @@ class CronController extends Controller
$return = [];
$return['recurring_transactions'] = $this->runRecurring($config['force'], $config['date']);
$return['auto_budgets'] = $this->runAutoBudget($config['force'], $config['date']);
if (true === config('cer.enabled')) {
$return['exchange_rates'] = $this->exchangeRatesCronJob($config['force'], $config['date']);
}
$return['bill_warnings'] = $this->billWarningCronJob($config['force'], $config['date']);
return response()->json($return);
}

View File

@ -79,9 +79,9 @@ class StoreRequest extends FormRequest
'currency_code' => 'exists:transaction_currencies,code',
'notes' => 'nullable|between:1,65536',
// auto budget info
'auto_budget_type' => 'in:reset,rollover,none',
'auto_budget_amount' => 'numeric|min:0|max:1000000000|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover',
'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover',
'auto_budget_type' => 'in:reset,rollover,adjusted,none',
'auto_budget_amount' => 'numeric|min:0|max:1000000000|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted',
'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted',
];
}

View File

@ -67,6 +67,7 @@ class UpdateRequest extends FormRequest
'none' => 0,
'reset' => 1,
'rollover' => 2,
'adjusted' => 3,
];
$allData['auto_budget_type'] = $types[$allData['auto_budget_type']] ?? 0;
}
@ -88,7 +89,7 @@ class UpdateRequest extends FormRequest
'name' => sprintf('between:1,100|uniqueObjectForUser:budgets,name,%d', $budget->id),
'active' => [new IsBoolean()],
'notes' => 'nullable|between:1,65536',
'auto_budget_type' => 'in:reset,rollover,none',
'auto_budget_type' => 'in:reset,rollover,adjusted,none',
'auto_budget_currency_id' => 'exists:transaction_currencies,id',
'auto_budget_currency_code' => 'exists:transaction_currencies,code',
'auto_budget_amount' => 'min:0|max:1000000000',

View File

@ -77,7 +77,7 @@ class Cron extends Command
*/
if (true === config('cer.enabled')) {
try {
$this->exchangeRatesCronJob($force, $date);
//$this->exchangeRatesCronJob($force, $date);
} catch (FireflyException $e) {
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
@ -89,7 +89,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());
@ -111,7 +111,7 @@ class Cron extends Command
* Fire bill warning cron job
*/
try {
$this->billWarningCronJob($force, $date);
//$this->billWarningCronJob($force, $date);
} catch (FireflyException $e) {
Log::error($e->getMessage());
Log::error($e->getTraceAsString());

View File

@ -31,4 +31,5 @@ enum AutoBudgetType: int
{
case AUTO_BUDGET_RESET = 1;
case AUTO_BUDGET_ROLLOVER = 2;
case AUTO_BUDGET_ADJUSTED = 3;
}

View File

@ -78,6 +78,7 @@ class CreateController extends Controller
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'),
AutoBudget::AUTO_BUDGET_ADJUSTED => (string)trans('firefly.auto_budget_adjusted'),
];
$autoBudgetPeriods = [
'daily' => (string)trans('firefly.auto_budget_period_daily'),

View File

@ -82,6 +82,7 @@ class EditController extends Controller
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'),
AutoBudget::AUTO_BUDGET_ADJUSTED => (string)trans('firefly.auto_budget_adjusted'),
];
$autoBudgetPeriods = [
'daily' => (string)trans('firefly.auto_budget_period_daily'),

View File

@ -65,7 +65,7 @@ class BudgetFormStoreRequest extends FormRequest
return [
'name' => 'required|between:1,100|uniqueObjectForUser:budgets,name',
'active' => 'numeric|between:0,1',
'auto_budget_type' => 'numeric|integer|gte:0|lte:2',
'auto_budget_type' => 'numeric|integer|gte:0|lte:3',
'auto_budget_currency_id' => 'exists:transaction_currencies,id',
'auto_budget_amount' => 'min:0|max:1000000000|required_if:auto_budget_type,1|required_if:auto_budget_type,2',
'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly',

View File

@ -81,6 +81,70 @@ class CreateAutoBudgetLimits implements ShouldQueue
}
}
/**
* @param AutoBudget $autoBudget
* @return void
*/
private function createAdjustedLimit(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 standard amount and we're done.
$this->createBudgetLimit($autoBudget, $start, $end);
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));
// what you spent in previous period PLUS the amount for the current period,
// if that is more than zero, that's the amount that will be set.
$budgetAvailable = bcadd(bcadd($budgetLimit->amount, $autoBudget->amount), $spentAmount);
$totalAmount = $autoBudget->amount;
Log::debug(sprintf('Total amount available for current budget period is %s', $budgetAvailable));
if (-1 !== bccomp( $budgetAvailable, $totalAmount)) {
Log::info(sprintf('There is no overspending, no need to adjust. Budget limit amount will be %s.', $totalAmount));
// create budget limit:
$this->createBudgetLimit($autoBudget, $start, $end, $totalAmount);
}
if (1 !== bccomp($budgetAvailable, $totalAmount)) {
Log::info(sprintf('There was overspending, so the new amount will be %s.', $budgetAvailable));
// create budget limit:
$this->createBudgetLimit($autoBudget, $start, $end, $budgetAvailable);
}
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
}
/**
* @param AutoBudget $autoBudget
*
@ -148,6 +212,13 @@ class CreateAutoBudgetLimits implements ShouldQueue
return;
}
if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ADJUSTED === (int)$autoBudget->auto_budget_type) {
// budget limit exists already,
$this->createAdjustedLimit($autoBudget);
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
return;
}
Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id));
}

View File

@ -69,6 +69,7 @@ class AutoBudget extends Model
public const AUTO_BUDGET_RESET = 1;
public const AUTO_BUDGET_ROLLOVER = 2;
public const AUTO_BUDGET_ADJUSTED = 3;
protected $fillable = ['budget_id','amount','period'];

View File

@ -829,6 +829,9 @@ class BudgetRepository implements BudgetRepositoryInterface
if ('rollover' === $type) {
$type = AutoBudget::AUTO_BUDGET_ROLLOVER;
}
if('adjusted' === $type) {
$type = AutoBudget::AUTO_BUDGET_ADJUSTED;
}
$repos = app(CurrencyRepositoryInterface::class);
$currency = null;

View File

@ -78,6 +78,7 @@ class BudgetTransformer extends AbstractTransformer
$types = [
AutoBudget::AUTO_BUDGET_RESET => 'reset',
AutoBudget::AUTO_BUDGET_ROLLOVER => 'rollover',
AutoBudget::AUTO_BUDGET_ADJUSTED => 'adjusted',
];
if (null !== $autoBudget) {

View File

@ -1656,6 +1656,7 @@ return [
'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_adjusted' => 'Add an amount every period and correct for overspending',
'auto_budget_period_daily' => 'Daily',
'auto_budget_period_weekly' => 'Weekly',
'auto_budget_period_monthly' => 'Monthly',
@ -1665,6 +1666,7 @@ return [
'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',
'auto_budget_adjusted_icon' => 'The budget amount will increase periodically and will correct for overspending',
'remove_budgeted_amount' => 'Remove budgeted amount in :currency',
// bills:

View File

@ -239,6 +239,9 @@
{% if 2 == budget.auto_budget.auto_budget_type %}
<span class="fa fa-fw fa-calendar-plus-o" title="{{ 'auto_budget_rollover_icon'|_ }}"></span>
{% endif %}
{% if 3 == budget.auto_budget.auto_budget_type %}
<span class="fa fa-fw fa-calendar-plus-o" title="{{ 'auto_budget_adjusted_icon'|_ }}"></span>
{% endif %}
{% endif %}
{% if budget.attachments.count() > 0 %}
<span class="fa fa-paperclip"></span>