billable timesheets, inactive projects report, bookmark export search (#2503)
This commit is contained in:
@@ -20,6 +20,7 @@ use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
@@ -97,9 +98,9 @@ class ActivityRepository extends EntityRepository
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('SUM(t.duration) as duration')
|
||||
->addSelect('SUM(t.rate) as rate')
|
||||
->addSelect('SUM(t.internalRate) as internal_rate')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internal_rate')
|
||||
->where('t.activity = :activity')
|
||||
->setParameter('activity', $activity)
|
||||
;
|
||||
@@ -115,6 +116,26 @@ class ActivityRepository extends EntityRepository
|
||||
$stats->setRecordInternalRate($timesheetResult['internal_rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->where('t.activity = :activity')
|
||||
->andWhere('t.billable = :billable')
|
||||
->setParameter('activity', $activity)
|
||||
->setParameter('billable', true, Types::BOOLEAN)
|
||||
;
|
||||
|
||||
$timesheetResult = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
if (null !== $timesheetResult) {
|
||||
$stats->setDurationBillable($timesheetResult['duration']);
|
||||
$stats->setRateBillable($timesheetResult['rate']);
|
||||
$stats->setRecordAmountBillable($timesheetResult['amount']);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
@@ -78,22 +79,16 @@ class CustomerRepository extends EntityRepository
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves statistics for one customer.
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @return CustomerStatistic
|
||||
*/
|
||||
public function getCustomerStatistics(Customer $customer)
|
||||
public function getCustomerStatistics(Customer $customer): CustomerStatistic
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('SUM(t.duration) as duration')
|
||||
->addSelect('SUM(t.rate) as rate')
|
||||
->addSelect('SUM(t.internalRate) as internal_rate')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internal_rate')
|
||||
->andWhere('p.customer = :customer')
|
||||
->setParameter('customer', $customer)
|
||||
;
|
||||
@@ -109,6 +104,27 @@ class CustomerRepository extends EntityRepository
|
||||
$stats->setRecordInternalRate($timesheetResult['internal_rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->andWhere('p.customer = :customer')
|
||||
->andWhere('t.billable = :billable')
|
||||
->setParameter('customer', $customer)
|
||||
->setParameter('billable', true, Types::BOOLEAN)
|
||||
;
|
||||
|
||||
$timesheetResult = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
if (null !== $timesheetResult) {
|
||||
$stats->setDurationBillable($timesheetResult['duration']);
|
||||
$stats->setRateBillable($timesheetResult['rate']);
|
||||
$stats->setRecordAmountBillable($timesheetResult['amount']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->select('COUNT(a.id) as amount')
|
||||
|
||||
@@ -22,6 +22,7 @@ use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
@@ -84,18 +85,14 @@ class ProjectRepository extends EntityRepository
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('SUM(t.duration) as duration')
|
||||
->addSelect('SUM(t.rate) as rate')
|
||||
->addSelect('SUM(t.internalRate) as internal_rate')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internal_rate')
|
||||
->andWhere('t.project = :project')
|
||||
->setParameter('project', $project)
|
||||
;
|
||||
|
||||
if (null !== $begin) {
|
||||
$qb->andWhere($qb->expr()->gte('t.begin', ':begin'))
|
||||
->setParameter('begin', $begin);
|
||||
}
|
||||
|
||||
// to calculate a budget at a certain point in time
|
||||
if (null !== $end) {
|
||||
$qb->andWhere($qb->expr()->lte('t.end', ':end'))
|
||||
->setParameter('end', $end);
|
||||
@@ -112,6 +109,32 @@ class ProjectRepository extends EntityRepository
|
||||
$stats->setRecordInternalRate($timesheetResult['internal_rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->andWhere('t.project = :project')
|
||||
->andWhere('t.billable = :billable')
|
||||
->setParameter('project', $project)
|
||||
->setParameter('billable', true, Types::BOOLEAN)
|
||||
;
|
||||
|
||||
// to calculate a budget at a certain point in time
|
||||
if (null !== $end) {
|
||||
$qb->andWhere($qb->expr()->lte('t.end', ':end'))
|
||||
->setParameter('end', $end);
|
||||
}
|
||||
|
||||
$timesheetResult = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
if (null !== $timesheetResult) {
|
||||
$stats->setDurationBillable($timesheetResult['duration']);
|
||||
$stats->setRateBillable($timesheetResult['rate']);
|
||||
$stats->setRecordAmountBillable($timesheetResult['amount']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Activity::class, 'a')
|
||||
|
||||
@@ -304,6 +304,11 @@ class BaseQuery
|
||||
return $this->bookmark;
|
||||
}
|
||||
|
||||
public function hasBookmark(): bool
|
||||
{
|
||||
return null !== $this->bookmark;
|
||||
}
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
@@ -245,10 +245,10 @@ class TimesheetRepository extends EntityRepository
|
||||
return $this->getDailyStats($user, $begin, $end);
|
||||
|
||||
case self::STATS_QUERY_DURATION:
|
||||
$what = 'SUM(t.duration)';
|
||||
$what = 'COALESCE(SUM(t.duration), 0)';
|
||||
break;
|
||||
case self::STATS_QUERY_RATE:
|
||||
$what = 'SUM(t.rate)';
|
||||
$what = 'COALESCE(SUM(t.rate), 0)';
|
||||
break;
|
||||
case self::STATS_QUERY_USER:
|
||||
$what = 'COUNT(DISTINCT(t.user))';
|
||||
@@ -271,10 +271,16 @@ class TimesheetRepository extends EntityRepository
|
||||
*/
|
||||
protected function queryThisMonth($select, User $user)
|
||||
{
|
||||
$begin = new DateTime('first day of this month 00:00:00');
|
||||
$end = new DateTime('last day of this month 23:59:59');
|
||||
try {
|
||||
$timezone = new \DateTimeZone($user->getTimezone());
|
||||
$begin = new DateTime('first day of this month 00:00:00', $timezone);
|
||||
$end = new DateTime('last day of this month 23:59:59', $timezone);
|
||||
|
||||
return $this->queryTimeRange($select, $begin, $end, $user);
|
||||
return $this->queryTimeRange($select, $begin, $end, $user);
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,20 +320,13 @@ class TimesheetRepository extends EntityRepository
|
||||
return empty($result) ? 0 : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch statistic data for one user.
|
||||
*
|
||||
* @param User $user
|
||||
* @return TimesheetStatistic
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getUserStatistics(User $user)
|
||||
public function getUserStatistics(User $user): TimesheetStatistic
|
||||
{
|
||||
$durationTotal = $this->getStatistic(self::STATS_QUERY_DURATION, null, null, $user);
|
||||
$recordsTotal = $this->getStatistic(self::STATS_QUERY_AMOUNT, null, null, $user);
|
||||
$rateTotal = $this->getStatistic(self::STATS_QUERY_RATE, null, null, $user);
|
||||
$amountMonth = $this->queryThisMonth('SUM(t.rate)', $user);
|
||||
$durationMonth = $this->queryThisMonth('SUM(t.duration)', $user);
|
||||
$amountMonth = $this->queryThisMonth('COALESCE(SUM(t.rate), 0)', $user);
|
||||
$durationMonth = $this->queryThisMonth('COALESCE(SUM(t.duration), 0)', $user);
|
||||
$firstEntry = $this->getEntityManager()
|
||||
->createQuery('SELECT MIN(t.begin) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
|
||||
->setParameter('user', $user)
|
||||
@@ -352,42 +351,15 @@ class TimesheetRepository extends EntityRepository
|
||||
* @param DateTime|null $end
|
||||
* @return Year[]
|
||||
*/
|
||||
public function getMonthlyStats(User $user = null, ?DateTime $begin = null, ?DateTime $end = null)
|
||||
public function getMonthlyStats(User $user = null, ?DateTime $begin = null, ?DateTime $end = null): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('SUM(t.rate) as rate, SUM(t.duration) as duration, MONTH(t.begin) as month, YEAR(t.begin) as year')
|
||||
->from(Timesheet::class, 't')
|
||||
;
|
||||
|
||||
if (!empty($begin)) {
|
||||
$qb->andWhere($qb->expr()->gte('t.begin', ':from'))
|
||||
->setParameter('from', $begin);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.begin'));
|
||||
}
|
||||
|
||||
if (!empty($end)) {
|
||||
$qb->andWhere($qb->expr()->lte('t.end', ':to'))
|
||||
->setParameter('to', $end);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
$qb
|
||||
->orderBy('year', 'DESC')
|
||||
->addOrderBy('month', 'ASC')
|
||||
->groupBy('year')
|
||||
->addGroupBy('month');
|
||||
|
||||
/** @var Year[] $years */
|
||||
$years = [];
|
||||
|
||||
$qb = $this->getMonthlyStatsQuery($user, $begin, $end, null);
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
$curYear = $statRow['year'];
|
||||
$curMonth = (int) $statRow['month'];
|
||||
|
||||
if (!isset($years[$curYear])) {
|
||||
$year = new Year($curYear);
|
||||
@@ -398,15 +370,74 @@ class TimesheetRepository extends EntityRepository
|
||||
$years[$curYear] = $year;
|
||||
}
|
||||
|
||||
$month = new Month($statRow['month']);
|
||||
$month->setTotalDuration((int) $statRow['duration'])
|
||||
->setTotalRate((float) $statRow['rate']);
|
||||
$years[$curYear]->setMonth($month);
|
||||
$month = $years[$curYear]->getMonth($curMonth);
|
||||
$month->setTotalDuration((int) $statRow['duration']);
|
||||
$month->setTotalRate((float) $statRow['rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getMonthlyStatsQuery($user, $begin, $end, true);
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
$curYear = $statRow['year'];
|
||||
$curMonth = (int) $statRow['month'];
|
||||
|
||||
if (!isset($years[$curYear])) {
|
||||
$year = new Year($curYear);
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
$month = $i < 10 ? '0' . $i : (string) $i;
|
||||
$year->setMonth(new Month($month));
|
||||
}
|
||||
$years[$curYear] = $year;
|
||||
}
|
||||
|
||||
$month = $years[$curYear]->getMonth($curMonth);
|
||||
$month->setBillableDuration((int) $statRow['duration']);
|
||||
$month->setBillableRate((float) $statRow['rate']);
|
||||
}
|
||||
|
||||
return $years;
|
||||
}
|
||||
|
||||
private function getMonthlyStatsQuery(User $user = null, ?DateTime $begin = null, ?DateTime $end = null, ?bool $billable = null): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->from(Timesheet::class, 't');
|
||||
$qb->select('COALESCE(SUM(t.rate), 0) as rate, COALESCE(SUM(t.duration), 0) as duration, MONTH(t.begin) as month, YEAR(t.begin) as year');
|
||||
|
||||
if (!empty($begin)) {
|
||||
$qb->andWhere($qb->expr()->gte('t.begin', ':from'));
|
||||
$qb->setParameter('from', $begin);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.begin'));
|
||||
}
|
||||
|
||||
if (!empty($end)) {
|
||||
$qb->andWhere($qb->expr()->lte('t.end', ':to'));
|
||||
$qb->setParameter('to', $end);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user');
|
||||
$qb->setParameter('user', $user);
|
||||
}
|
||||
|
||||
if (null !== $billable) {
|
||||
$qb->andWhere('t.billable = :billable');
|
||||
$qb->setParameter('billable', $billable);
|
||||
}
|
||||
|
||||
$qb
|
||||
->orderBy('year', 'DESC')
|
||||
->addOrderBy('month', 'ASC')
|
||||
->groupBy('year')
|
||||
->addGroupBy('month')
|
||||
;
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* In case this method is called with one timezone and the results are from another timezone,
|
||||
* it might return rows outside the time-range.
|
||||
@@ -481,6 +512,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$results[$dateKey] = [
|
||||
'rate' => 0,
|
||||
'duration' => 0,
|
||||
'billable' => 0, // duration
|
||||
'month' => $beginTmp->format('n'),
|
||||
'year' => $beginTmp->format('Y'),
|
||||
'day' => $beginTmp->format('j'),
|
||||
@@ -496,6 +528,9 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
$results[$dateKey]['rate'] += $rate;
|
||||
$results[$dateKey]['duration'] += $duration;
|
||||
if ($result->isBillable()) {
|
||||
$results[$dateKey]['billable'] += $duration;
|
||||
}
|
||||
$detailsId =
|
||||
$result->getProject()->getCustomer()->getId()
|
||||
. '_' . $result->getProject()->getId()
|
||||
@@ -508,11 +543,15 @@ class TimesheetRepository extends EntityRepository
|
||||
'activity' => $result->getActivity(),
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'billable' => 0, // duration
|
||||
];
|
||||
}
|
||||
|
||||
$results[$dateKey]['details'][$detailsId]['duration'] += $duration;
|
||||
$results[$dateKey]['details'][$detailsId]['rate'] += $rate;
|
||||
if ($result->isBillable()) {
|
||||
$results[$dateKey]['details'][$detailsId]['billable'] += $duration;
|
||||
}
|
||||
}
|
||||
|
||||
$beginTmp = $newDateBegin;
|
||||
@@ -562,6 +601,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$dateTime->setDate($statRow['year'], $statRow['month'], $statRow['day']);
|
||||
$dateTime->setTime(0, 0, 0);
|
||||
$day = new Day($dateTime, (int) $statRow['duration'], (float) $statRow['rate']);
|
||||
$day->setTotalDurationBillable($statRow['billable']);
|
||||
$day->setDetails($statRow['details']);
|
||||
$dateKey = $dateTime->format('Ymd');
|
||||
// make sure entries from other timezones are filtered
|
||||
|
||||
Reference in New Issue
Block a user