monthly budget, monthly report, unified report calculation (#2684)

This commit is contained in:
Kevin Papst
2021-08-06 18:38:41 +02:00
committed by GitHub
parent 21f785638c
commit cefd747e91
196 changed files with 5031 additions and 2167 deletions

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
use App\Entity\Activity;
/**
* Object used to unify the access to budget data in charts.
*
* @internal do not use in plugins, no BC promise given!
* @method Activity getEntity()
*/
class ActivityBudgetStatisticModel extends BudgetStatisticModel
{
public function __construct(Activity $activity)
{
parent::__construct($activity);
}
public function getActivity(): Activity
{
return $this->getEntity();
}
}

View File

@@ -10,8 +10,9 @@
namespace App\Model;
use App\Entity\Activity;
use App\Model\Statistic\BudgetStatistic;
class ActivityStatistic extends TimesheetCountedStatistic implements \JsonSerializable
class ActivityStatistic extends BudgetStatistic implements \JsonSerializable
{
/**
* @var Activity

View File

@@ -0,0 +1,198 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
use App\Entity\EntityWithBudget;
use App\Model\Statistic\BudgetStatistic;
/**
* Object used to unify the access to budget data in charts.
*
* @internal do not use in plugins, no BC promise given!
*/
class BudgetStatisticModel implements BudgetStatisticModelInterface
{
/**
* @var EntityWithBudget
*/
private $entity;
/**
* @var BudgetStatistic
*/
private $statistic;
/**
* @var BudgetStatistic
*/
private $statisticTotal;
public function __construct(EntityWithBudget $entity)
{
$this->entity = $entity;
}
public function getEntity(): EntityWithBudget
{
return $this->entity;
}
public function getStatistic(): ?BudgetStatistic
{
return $this->statistic;
}
public function setStatistic(BudgetStatistic $statistic)
{
$this->statistic = $statistic;
}
public function getStatisticTotal(): ?BudgetStatistic
{
return $this->statisticTotal;
}
public function setStatisticTotal(BudgetStatistic $statistic)
{
$this->statisticTotal = $statistic;
}
public function hasTimeBudget(): bool
{
return $this->entity->hasTimeBudget();
}
public function getTimeBudget(): int
{
return $this->entity->getTimeBudget();
}
public function isMonthlyBudget(): bool
{
return $this->entity->isMonthlyBudget();
}
public function getDurationBillable(): int
{
if ($this->isMonthlyBudget()) {
if ($this->statistic === null) {
return 0;
}
return $this->statistic->getDurationBillable();
}
if ($this->statisticTotal === null) {
return 0;
}
return $this->statisticTotal->getDurationBillable();
}
public function getTimeBudgetOpen(): int
{
$value = $this->getTimeBudget() - $this->getTimeBudgetSpent();
return $value > 0 ? $value : 0;
}
public function getTimeBudgetSpent(): int
{
return $this->getDurationBillable();
}
public function hasBudget(): bool
{
return $this->entity->hasBudget();
}
public function getBudget(): float
{
return $this->entity->getBudget();
}
public function getBudgetOpen(): float
{
$value = $this->getBudget() - $this->getBudgetSpent();
return $value > 0 ? $value : 0;
}
public function getBudgetSpent(): float
{
return $this->getRateBillable();
}
public function getRateBillable(): float
{
if ($this->isMonthlyBudget()) {
if ($this->statistic === null) {
return 0.00;
}
return $this->statistic->getRateBillable();
}
if ($this->statisticTotal === null) {
return 0.00;
}
return $this->statisticTotal->getRateBillable();
}
public function getRate(): float
{
if ($this->isMonthlyBudget()) {
if ($this->statistic === null) {
return 0.00;
}
return $this->statistic->getRate();
}
if ($this->statisticTotal === null) {
return 0.00;
}
return $this->statisticTotal->getRate();
}
public function getDuration(): int
{
if ($this->isMonthlyBudget()) {
if ($this->statistic === null) {
return 0;
}
return $this->statistic->getDuration();
}
if ($this->statisticTotal === null) {
return 0;
}
return $this->statisticTotal->getDuration();
}
public function getInternalRate(): float
{
if ($this->isMonthlyBudget()) {
if ($this->statistic === null) {
return 0.00;
}
return $this->statistic->getInternalRate();
}
if ($this->statisticTotal === null) {
return 0.00;
}
return $this->statisticTotal->getInternalRate();
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
/**
* @internal do not use in plugins, no BC promise given!
*/
interface BudgetStatisticModelInterface
{
public function isMonthlyBudget(): bool;
public function hasTimeBudget(): bool;
public function getTimeBudget(): int;
public function getTimeBudgetSpent(): int;
public function hasBudget(): bool;
public function getBudget(): float;
public function getBudgetSpent(): float;
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
use App\Entity\Customer;
/**
* Object used to unify the access to budget data in charts.
*
* @internal do not use in plugins, no BC promise given!
* @method Customer getEntity()
*/
class CustomerBudgetStatisticModel extends BudgetStatisticModel
{
public function __construct(Customer $customer)
{
parent::__construct($customer);
}
public function getCustomer(): Customer
{
return $this->getEntity();
}
}

View File

@@ -9,7 +9,9 @@
namespace App\Model;
class CustomerStatistic extends TimesheetCountedStatistic
use App\Model\Statistic\BudgetStatistic;
class CustomerStatistic extends BudgetStatistic
{
/**
* @var int
@@ -20,11 +22,20 @@ class CustomerStatistic extends TimesheetCountedStatistic
*/
private $projectAmount = 0;
/**
* @deprecated since 1.15 - will be removed with 2.0
* @return int
*/
public function getActivityAmount(): int
{
return $this->activityAmount;
}
/**
* @deprecated since 1.15 - will be removed with 2.0
* @param int $activityAmount
* @return $this
*/
public function setActivityAmount(int $activityAmount): CustomerStatistic
{
$this->activityAmount = $activityAmount;
@@ -32,11 +43,20 @@ class CustomerStatistic extends TimesheetCountedStatistic
return $this;
}
/**
* @deprecated since 1.15 - will be removed with 2.0
* @return int
*/
public function getProjectAmount(): int
{
return $this->projectAmount;
}
/**
* @deprecated since 1.15 - will be removed with 2.0
* @param int $projectAmount
* @return $this
*/
public function setProjectAmount(int $projectAmount): CustomerStatistic
{
$this->projectAmount = $projectAmount;

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
use App\Entity\User;
use App\Model\Statistic\StatisticDate;
use DateTime;
use DateTimeInterface;
final class DailyStatistic
{
/**
* @var array<string, StatisticDate>
*/
private $days = [];
private $begin;
private $end;
private $user;
public function __construct(DateTime $begin, DateTime $end, User $user)
{
$this->begin = clone $begin;
$this->end = clone $end;
$this->user = $user;
}
public function getUser(): User
{
return $this->user;
}
private function setupDays(): void
{
if (!empty($this->days)) {
return;
}
$tmp = clone $this->begin;
$tmp->setTime(0, 0, 0);
while ($tmp < $this->end) {
$id = $tmp->format('Y-m-d');
$this->days[$id] = new StatisticDate(clone $tmp);
$tmp->modify('+1 day');
}
}
/**
* @return StatisticDate[]
*/
public function getDays(): array
{
$this->setupDays();
return array_values($this->days);
}
public function getDayByDateTime(\DateTimeInterface $date): ?StatisticDate
{
return $this->getDay($date->format('Y'), $date->format('m'), $date->format('d'));
}
public function getDayByReportDate(string $date): ?StatisticDate
{
$this->setupDays();
if (!isset($this->days[$date])) {
return null;
}
return $this->days[$date];
}
public function getDay(string $year, string $month, string $day): ?StatisticDate
{
if ((int) $month < 10) {
$month = '0' . (int) $month;
}
if ((int) $day < 10) {
$day = '0' . (int) $day;
}
$date = $year . '-' . $month . '-' . $day;
return $this->getDayByReportDate($date);
}
/**
* @return DateTimeInterface[]
*/
public function getDateTimes(): array
{
$this->setupDays();
$all = [];
foreach ($this->days as $id => $day) {
$all[] = $day->getDate();
}
return $all;
}
}

View File

@@ -0,0 +1,139 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
use App\Entity\User;
use App\Model\Statistic\StatisticDate;
use DateTime;
use DateTimeInterface;
final class MonthlyStatistic
{
/**
* @var array<string, array<int, StatisticDate>>
*/
private $years = [];
private $begin;
private $end;
private $user;
public function __construct(DateTime $begin, DateTime $end, User $user)
{
$this->begin = clone $begin;
$this->end = clone $end;
$this->user = $user;
}
public function getUser(): User
{
return $this->user;
}
private function setupYears(): void
{
if (!empty($this->years)) {
return;
}
$years = [];
$begin = clone $this->begin;
$begin->setTime(0, 0, 0);
$tmp = clone $begin;
$day = (int) $begin->format('d');
while ($tmp < $this->end) {
$curYear = $tmp->format('Y');
if (!isset($years[$curYear])) {
$year = [];
for ($i = 1; $i < 13; $i++) {
$date = clone $begin;
// financial years do NOT start at the first of the month, do not reset day to 1
$date->setDate((int) $curYear, $i, $day);
$date->setTime(0, 0, 0);
if ($date < $begin || $date > $this->end) {
continue;
}
$year[$i] = new StatisticDate($date);
}
$years[$curYear] = $year;
}
$tmp->modify('+1 month');
}
$this->years = $years;
}
/**
* @return string[]
*/
public function getYears(): array
{
$this->setupYears();
return array_keys($this->years);
}
public function getYear(string $year): ?array
{
$this->setupYears();
if (!isset($this->years[$year])) {
return null;
}
return $this->years[$year];
}
/**
* @return StatisticDate[]
*/
public function getMonths(): array
{
$this->setupYears();
$all = [];
foreach ($this->years as $number => $months) {
foreach ($months as $monthNumber => $statisticDate) {
$all[] = $statisticDate;
}
}
return $all;
}
public function getMonth(string $year, string $month): ?StatisticDate
{
$this->setupYears();
$month = (int) $month;
if (!isset($this->years[$year]) || !isset($this->years[$year][$month])) {
return null;
}
return $this->years[$year][$month];
}
/**
* @return DateTimeInterface[]
*/
public function getDateTimes(): array
{
$this->setupYears();
$all = [];
foreach ($this->years as $number => $months) {
foreach ($months as $monthNumber => $statisticDate) {
$all[] = $statisticDate->getDate();
}
}
return $all;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
use App\Entity\Project;
/**
* Object used to unify the access to budget data in charts.
*
* @internal do not use in plugins, no BC promise given!
* @method Project getEntity()
*/
class ProjectBudgetStatisticModel extends BudgetStatisticModel
{
public function __construct(Project $project)
{
parent::__construct($project);
}
public function getProject(): Project
{
return $this->getEntity();
}
}

View File

@@ -9,18 +9,29 @@
namespace App\Model;
class ProjectStatistic extends TimesheetCountedStatistic
use App\Model\Statistic\BudgetStatistic;
class ProjectStatistic extends BudgetStatistic
{
/**
* @var int
*/
private $activityAmount = 0;
/**
* @deprecated since 1.15 - will be removed with 2.0
* @return int
*/
public function getActivityAmount(): int
{
return $this->activityAmount;
}
/**
* @deprecated since 1.15 - will be removed with 2.0
* @param int $activityAmount
* @return $this
*/
public function setActivityAmount(int $activityAmount): ProjectStatistic
{
$this->activityAmount = $activityAmount;

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model\Statistic;
use App\Model\TimesheetCountedStatistic;
/**
* @final
*/
class BudgetStatistic extends TimesheetCountedStatistic
{
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model\Statistic;
final class StatisticDate extends Timesheet
{
private $date;
private $billableDuration = 0;
private $billableRate = 0.00;
public function __construct(\DateTimeInterface $date)
{
$this->date = clone $date;
}
public function getDate(): \DateTimeInterface
{
return $this->date;
}
public function getBillableDuration(): int
{
return $this->billableDuration;
}
public function setBillableDuration(int $billableDuration): void
{
$this->billableDuration = $billableDuration;
}
public function getBillableRate(): float
{
return $this->billableRate;
}
public function setBillableRate(float $billableRate): void
{
$this->billableRate = $billableRate;
}
}

View File

@@ -22,7 +22,7 @@ class Timesheet
*/
public function getValue(): int
{
return $this->totalDuration;
return $this->getDuration();
}
/**
@@ -37,7 +37,7 @@ class Timesheet
public function getTotalDuration(): int
{
return $this->totalDuration;
return $this->getDuration();
}
public function setTotalDuration(int $totalDuration): void
@@ -57,7 +57,7 @@ class Timesheet
public function getTotalRate(): float
{
return $this->totalRate;
return $this->getRate();
}
public function setTotalRate(float $totalRate): void
@@ -77,7 +77,7 @@ class Timesheet
public function getTotalInternalRate(): float
{
return $this->totalInternalRate;
return $this->getInternalRate();
}
public function setTotalInternalRate(float $totalInternalRate): void

View File

@@ -11,22 +11,50 @@ namespace App\Model;
class TimesheetCountedStatistic implements \JsonSerializable
{
private $recordAmount = 0;
private $counter = 0;
private $recordDuration = 0;
private $recordRate = 0.0;
private $recordInternalRate = 0.0;
private $recordAmountBillable = 0;
private $counterBillable = 0;
private $recordDurationBillable = 0;
private $recordRateBillable = 0.0;
private $internalRateBillable = 0.0;
/**
* For unified access, used in frontend.
*
* @return int
*/
public function getCounter(): int
{
return $this->counter;
}
public function setCounter(int $counter): void
{
$this->counter = $counter;
}
public function getCounterBillable(): int
{
return $this->counterBillable;
}
public function setCounterBillable(int $counter): void
{
$this->counterBillable = $counter;
}
/**
* Returns the total amount of included timesheet records.
*
* @return int
* @deprecated since 1.15 use getCounter() instead
*/
public function getRecordAmount()
{
return $this->recordAmount;
return $this->getCounter();
}
/**
@@ -35,7 +63,7 @@ class TimesheetCountedStatistic implements \JsonSerializable
*/
public function setRecordAmount($recordAmount)
{
$this->recordAmount = (int) $recordAmount;
$this->setCounter((int) $recordAmount);
return $this;
}
@@ -47,7 +75,12 @@ class TimesheetCountedStatistic implements \JsonSerializable
*/
public function getValue(): int
{
return $this->recordDuration;
return $this->getDuration();
}
public function setDuration(int $duration): void
{
$this->recordDuration = $duration;
}
/**
@@ -65,9 +98,9 @@ class TimesheetCountedStatistic implements \JsonSerializable
*
* @return int
*/
public function getRecordDuration()
public function getRecordDuration(): int
{
return $this->recordDuration;
return $this->getDuration();
}
/**
@@ -76,7 +109,7 @@ class TimesheetCountedStatistic implements \JsonSerializable
*/
public function setRecordDuration($recordDuration)
{
$this->recordDuration = (int) $recordDuration;
$this->setDuration((int) $recordDuration);
return $this;
}
@@ -96,9 +129,14 @@ class TimesheetCountedStatistic implements \JsonSerializable
*
* @return float
*/
public function getRecordRate()
public function getRecordRate(): float
{
return $this->recordRate;
return $this->getRate();
}
public function setRate(float $rate): void
{
$this->recordRate = $rate;
}
/**
@@ -107,40 +145,66 @@ class TimesheetCountedStatistic implements \JsonSerializable
*/
public function setRecordRate($recordRate)
{
$this->recordRate = (float) $recordRate;
$this->setRate((float) $recordRate);
return $this;
}
/**
* @deprecated since 1.15 use getInternalRate() instead
*/
public function getRecordInternalRate(): float
{
return $this->getInternalRate();
}
/**
* Returns the total internal rate of all included timesheet records.
*
* @return float
*/
public function getRecordInternalRate()
public function getInternalRate(): float
{
return $this->recordInternalRate;
}
public function getInternalRateBillable(): float
{
return $this->internalRateBillable;
}
public function setInternalRateBillable(float $internalRateBillable): void
{
$this->internalRateBillable = $internalRateBillable;
}
/**
* @param float $recordInternalRate
* @return $this
*/
public function setRecordInternalRate($recordInternalRate)
{
$this->recordInternalRate = (float) $recordInternalRate;
$this->setInternalRate((float) $recordInternalRate);
return $this;
}
public function setInternalRate(float $internalRate): void
{
$this->recordInternalRate = $internalRate;
}
/**
* @deprecated since 1.15 use getCounterBillable() instead
*/
public function getRecordAmountBillable(): int
{
return $this->recordAmountBillable;
return $this->getCounterBillable();
}
public function setRecordAmountBillable(int $recordAmount): void
{
$this->recordAmountBillable = $recordAmount;
$this->setCounterBillable($recordAmount);
}
public function getDurationBillable(): int
@@ -171,8 +235,8 @@ class TimesheetCountedStatistic implements \JsonSerializable
'rate' => $this->recordRate,
'rate_billable' => $this->recordRateBillable,
'rate_internal' => $this->recordInternalRate,
'amount' => $this->recordAmount,
'amount_billable' => $this->recordAmountBillable,
'amount' => $this->counter,
'amount_billable' => $this->counterBillable,
];
}
}

View File

@@ -107,6 +107,9 @@ class TimesheetStatistic
$this->amountThisMonth = (float) $amountThisMonth;
}
/**
* @deprecated since 1.15 use TimesheetStatisticService::findFirstRecordDate() instead, will be removed with 2.0
*/
public function getFirstEntry(): ?\DateTime
{
return $this->firstEntry;

View File

@@ -31,10 +31,10 @@ class UserStatistic extends TimesheetCountedStatistic
public function addValuesFromMonth(Month $month): void
{
$this->setRecordDuration($this->getRecordDuration() + $month->getTotalDuration());
$this->setDuration($this->getDuration() + $month->getDuration());
$this->setDurationBillable($this->getDurationBillable() + $month->getBillableDuration());
$this->setRecordRate($this->getRate() + $month->getTotalRate());
$this->setRate($this->getRate() + $month->getRate());
$this->setRateBillable($this->getRateBillable() + $month->getBillableRate());
$this->setRecordInternalRate($this->getRecordInternalRate() + $month->getTotalInternalRate());
$this->setInternalRate($this->getInternalRate() + $month->getInternalRate());
}
}