Release 1.20.1 (#3317)

- improved timesheet calculator with changesets and priority
- calculate and include exported stats (e.g. available in export templates)
- added hourly rate column to timesheet listing
This commit is contained in:
Kevin Papst
2022-05-21 13:13:56 +02:00
committed by GitHub
parent 3e6100f368
commit d5f8655827
15 changed files with 277 additions and 43 deletions

View File

@@ -149,10 +149,20 @@ class ActivityStatisticService
$statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']); $statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']);
$statistic->setCounter($statistic->getCounter() + $resultRow['counter']); $statistic->setCounter($statistic->getCounter() + $resultRow['counter']);
if ($resultRow['billable']) { if ($resultRow['billable']) {
$statistic->setDurationBillable($resultRow['duration']); $statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']);
$statistic->setRateBillable($resultRow['rate']); $statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']);
$statistic->setInternalRateBillable($resultRow['internalRate']); $statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']);
$statistic->setCounterBillable($resultRow['counter']); $statistic->setCounterBillable($statistic->getCounterBillable() + $resultRow['counter']);
if ($resultRow['exported']) {
$statistic->setDurationBillableExported($statistic->getDurationBillableExported() + $resultRow['duration']);
$statistic->setRateBillableExported($statistic->getRateBillableExported() + $resultRow['rate']);
}
}
if ($resultRow['exported']) {
$statistic->setDurationExported($statistic->getDurationExported() + $resultRow['duration']);
$statistic->setRateExported($statistic->getRateExported() + $resultRow['rate']);
$statistic->setInternalRateExported($statistic->getInternalRateExported() + $resultRow['internalRate']);
$statistic->setCounterExported($statistic->getCounterExported() + $resultRow['counter']);
} }
} }
} }
@@ -170,9 +180,11 @@ class ActivityStatisticService
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate') ->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
->addSelect('COUNT(t.id) as counter') ->addSelect('COUNT(t.id) as counter')
->addSelect('t.billable as billable') ->addSelect('t.billable as billable')
->addSelect('t.exported as exported')
->andWhere($qb->expr()->isNotNull('t.end')) ->andWhere($qb->expr()->isNotNull('t.end'))
->groupBy('id') ->groupBy('id')
->addGroupBy('billable') ->addGroupBy('billable')
->addGroupBy('exported')
->andWhere($qb->expr()->in('t.activity', ':activity')) ->andWhere($qb->expr()->in('t.activity', ':activity'))
->setParameter('activity', $activities) ->setParameter('activity', $activities)
; ;

View File

@@ -17,11 +17,11 @@ class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '1.20'; public const VERSION = '1.20.1';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 12000; public const VERSION_ID = 12001;
/** /**
* The current release status, either "stable" or "dev" * The current release status, either "stable" or "dev"
*/ */

View File

@@ -97,10 +97,20 @@ class CustomerStatisticService
$statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']); $statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']);
$statistic->setCounter($statistic->getCounter() + $resultRow['counter']); $statistic->setCounter($statistic->getCounter() + $resultRow['counter']);
if ($resultRow['billable']) { if ($resultRow['billable']) {
$statistic->setDurationBillable($resultRow['duration']); $statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']);
$statistic->setRateBillable($resultRow['rate']); $statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']);
$statistic->setInternalRateBillable($resultRow['internalRate']); $statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']);
$statistic->setCounterBillable($resultRow['counter']); $statistic->setCounterBillable($statistic->getCounterBillable() + $resultRow['counter']);
if ($resultRow['exported']) {
$statistic->setDurationBillableExported($statistic->getDurationBillableExported() + $resultRow['duration']);
$statistic->setRateBillableExported($statistic->getRateBillableExported() + $resultRow['rate']);
}
}
if ($resultRow['exported']) {
$statistic->setDurationExported($statistic->getDurationExported() + $resultRow['duration']);
$statistic->setRateExported($statistic->getRateExported() + $resultRow['rate']);
$statistic->setInternalRateExported($statistic->getInternalRateExported() + $resultRow['internalRate']);
$statistic->setCounterExported($statistic->getCounterExported() + $resultRow['counter']);
} }
} }
} }
@@ -119,9 +129,11 @@ class CustomerStatisticService
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate') ->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
->addSelect('COUNT(t.id) as counter') ->addSelect('COUNT(t.id) as counter')
->addSelect('t.billable as billable') ->addSelect('t.billable as billable')
->addSelect('t.exported as exported')
->andWhere($qb->expr()->isNotNull('t.end')) ->andWhere($qb->expr()->isNotNull('t.end'))
->groupBy('id') ->groupBy('id')
->addGroupBy('billable') ->addGroupBy('billable')
->addGroupBy('exported')
->andWhere($qb->expr()->in('p.customer', ':customer')) ->andWhere($qb->expr()->in('p.customer', ':customer'))
->setParameter('customer', $customers) ->setParameter('customer', $customers)
; ;

View File

@@ -23,7 +23,11 @@ class TimesheetSubscriber implements EventSubscriber
/** /**
* @var CalculatorInterface[] * @var CalculatorInterface[]
*/ */
protected $calculator; private $calculator;
/**
* @var CalculatorInterface[]
*/
private $sorted;
/** /**
* @param CalculatorInterface[] $calculators * @param CalculatorInterface[] $calculators
@@ -33,20 +37,14 @@ class TimesheetSubscriber implements EventSubscriber
$this->calculator = $calculators; $this->calculator = $calculators;
} }
/** public function getSubscribedEvents(): array
* @return array
*/
public function getSubscribedEvents()
{ {
return [ return [
Events::onFlush, Events::onFlush,
]; ];
} }
/** public function onFlush(OnFlushEventArgs $args): void
* @param OnFlushEventArgs $args
*/
public function onFlush(OnFlushEventArgs $args)
{ {
$em = $args->getEntityManager(); $em = $args->getEntityManager();
$uow = $em->getUnitOfWork(); $uow = $em->getUnitOfWork();
@@ -57,7 +55,7 @@ class TimesheetSubscriber implements EventSubscriber
continue; continue;
} }
$this->calculateFields($entity); $this->calculateFields($entity, $uow->getEntityChangeSet($entity));
$uow->recomputeSingleEntityChangeSet($meta, $entity); $uow->recomputeSingleEntityChangeSet($meta, $entity);
} }
@@ -71,13 +69,31 @@ class TimesheetSubscriber implements EventSubscriber
} }
} }
/** protected function calculateFields(Timesheet $entity, array $changes = []): void
* @param Timesheet $entity
*/
protected function calculateFields(Timesheet $entity)
{ {
foreach ($this->calculator as $calculator) { if ($this->sorted === null) {
$calculator->calculate($entity); $this->sorted = [];
foreach ($this->calculator as $calculator) {
$i = 0;
$prio = 1000;
if (method_exists($calculator, 'getPriority')) {
$prio = $calculator->getPriority();
}
do {
$key = $prio + $i++;
} while (\array_key_exists($key, $this->sorted));
$this->sorted[$key] = $calculator;
}
ksort($this->sorted);
}
foreach ($this->sorted as $calculator) {
/* @phpstan-ignore-next-line */
$calculator->calculate($entity, $changes);
} }
} }
} }

View File

@@ -654,6 +654,14 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
return $this; return $this;
} }
public function resetRates(): void
{
$this->rate = 0.00;
$this->internalRate = null;
$this->hourlyRate = null;
$this->fixedRate = null;
}
public function isBillable(): bool public function isBillable(): bool
{ {
return $this->billable; return $this->billable;

View File

@@ -11,6 +11,7 @@ namespace App\Export\Base;
use App\Activity\ActivityStatisticService; use App\Activity\ActivityStatisticService;
use App\Invoice\InvoiceItemInterface; use App\Invoice\InvoiceItemInterface;
use App\Model\TimesheetCountedStatistic;
use App\Project\ProjectStatisticService; use App\Project\ProjectStatisticService;
use App\Repository\Query\TimesheetQuery; use App\Repository\Query\TimesheetQuery;
@@ -172,6 +173,7 @@ trait RendererTrait
{ {
$summary = []; $summary = [];
$projects = []; $projects = [];
$empty = new TimesheetCountedStatistic();
foreach ($exportItems as $exportItem) { foreach ($exportItems as $exportItem) {
$customer = null; $customer = null;
@@ -183,15 +185,14 @@ trait RendererTrait
$customer = $project->getCustomer(); $customer = $project->getCustomer();
$customerId = $customer->getId(); $customerId = $customer->getId();
$projectId = $project->getId(); $projectId = $project->getId();
if ($project->hasBudgets()) { $projects[] = $project;
$projects[] = $project;
}
} }
$id = $customerId . '_' . $projectId; $id = $customerId . '_' . $projectId;
if (!isset($summary[$id])) { if (!isset($summary[$id])) {
$summary[$id] = [ $summary[$id] = [
'totals' => $empty->jsonSerialize(),
'time' => $project->getTimeBudget(), 'time' => $project->getTimeBudget(),
'money' => $project->getBudget(), 'money' => $project->getBudget(),
'time_left' => null, 'time_left' => null,
@@ -209,6 +210,8 @@ trait RendererTrait
foreach ($allBudgets as $projectId => $statisticModel) { foreach ($allBudgets as $projectId => $statisticModel) {
$project = $statisticModel->getProject(); $project = $statisticModel->getProject();
$id = $project->getCustomer()->getId() . '_' . $projectId; $id = $project->getCustomer()->getId() . '_' . $projectId;
$total = $statisticModel->getStatisticTotal();
$summary[$id]['totals'] = $total->jsonSerialize();
if ($statisticModel->hasTimeBudget()) { if ($statisticModel->hasTimeBudget()) {
$summary[$id]['time_left'] = $statisticModel->getTimeBudgetOpenRelative(); $summary[$id]['time_left'] = $statisticModel->getTimeBudgetOpenRelative();
$summary[$id]['time_left_total'] = $statisticModel->getTimeBudgetOpen(); $summary[$id]['time_left_total'] = $statisticModel->getTimeBudgetOpen();
@@ -255,6 +258,7 @@ trait RendererTrait
{ {
$summary = []; $summary = [];
$activities = []; $activities = [];
$empty = new TimesheetCountedStatistic();
foreach ($exportItems as $exportItem) { foreach ($exportItems as $exportItem) {
$customerId = 'none'; $customerId = 'none';
@@ -270,9 +274,7 @@ trait RendererTrait
continue; continue;
} }
if ($activity->hasBudgets()) { $activities[] = $activity;
$activities[] = $activity;
}
if (null !== ($project = $exportItem->getProject())) { if (null !== ($project = $exportItem->getProject())) {
$projectId = $project->getId(); $projectId = $project->getId();
@@ -289,6 +291,7 @@ trait RendererTrait
if (!isset($summary[$id][$activityId])) { if (!isset($summary[$id][$activityId])) {
$summary[$id][$activityId] = [ $summary[$id][$activityId] = [
'totals' => $empty->jsonSerialize(),
'time' => $activity->getTimeBudget(), 'time' => $activity->getTimeBudget(),
'money' => $activity->getBudget(), 'money' => $activity->getBudget(),
'time_left' => null, 'time_left' => null,
@@ -306,6 +309,8 @@ trait RendererTrait
foreach ($allBudgets as $activityId => $statisticModel) { foreach ($allBudgets as $activityId => $statisticModel) {
$project = $statisticModel->getActivity()->getProject(); $project = $statisticModel->getActivity()->getProject();
$id = $project->getCustomer()->getId() . '_' . $project->getId(); $id = $project->getCustomer()->getId() . '_' . $project->getId();
$total = $statisticModel->getStatisticTotal();
$summary[$id][$activityId]['totals'] = $total->jsonSerialize();
if ($statisticModel->hasTimeBudget()) { if ($statisticModel->hasTimeBudget()) {
$summary[$id][$activityId]['time_left'] = $statisticModel->getTimeBudgetOpenRelative(); $summary[$id][$activityId]['time_left'] = $statisticModel->getTimeBudgetOpenRelative();
$summary[$id][$activityId]['time_left_total'] = $statisticModel->getTimeBudgetOpen(); $summary[$id][$activityId]['time_left_total'] = $statisticModel->getTimeBudgetOpen();

View File

@@ -21,6 +21,14 @@ class TimesheetCountedStatistic implements \JsonSerializable
private $recordRateBillable = 0.0; private $recordRateBillable = 0.0;
private $internalRateBillable = 0.0; private $internalRateBillable = 0.0;
private $recordRateBillableExported = 0.0;
private $recordDurationBillableExported = 0;
private $counterExported = 0;
private $recordDurationExported = 0;
private $recordRateExported = 0.0;
private $internalRateExported = 0.0;
/** /**
* For unified access, used in frontend. * For unified access, used in frontend.
* *
@@ -46,6 +54,16 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->counterBillable = $counter; $this->counterBillable = $counter;
} }
public function getCounterExported(): int
{
return $this->counterExported;
}
public function setCounterExported(int $counter): void
{
$this->counterExported = $counter;
}
/** /**
* Returns the total amount of included timesheet records. * Returns the total amount of included timesheet records.
* *
@@ -178,6 +196,16 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->internalRateBillable = $internalRateBillable; $this->internalRateBillable = $internalRateBillable;
} }
public function getInternalRateExported(): float
{
return $this->internalRateExported;
}
public function setInternalRateExported(float $internalRateExported): void
{
$this->internalRateExported = $internalRateExported;
}
/** /**
* @param float $recordInternalRate * @param float $recordInternalRate
* @return $this * @return $this
@@ -217,6 +245,16 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordDurationBillable = $recordDuration; $this->recordDurationBillable = $recordDuration;
} }
public function getDurationBillableExported(): int
{
return $this->recordDurationBillableExported;
}
public function setDurationBillableExported(int $recordDuration): void
{
$this->recordDurationBillableExported = $recordDuration;
}
public function getRateBillable(): float public function getRateBillable(): float
{ {
return $this->recordRateBillable; return $this->recordRateBillable;
@@ -227,16 +265,52 @@ class TimesheetCountedStatistic implements \JsonSerializable
$this->recordRateBillable = $recordRate; $this->recordRateBillable = $recordRate;
} }
public function getRateBillableExported(): float
{
return $this->recordRateBillableExported;
}
public function setRateBillableExported(float $recordRate): void
{
$this->recordRateBillableExported = $recordRate;
}
public function getDurationExported(): int
{
return $this->recordDurationExported;
}
public function setDurationExported(int $recordDuration): void
{
$this->recordDurationExported = $recordDuration;
}
public function getRateExported(): float
{
return $this->recordRateExported;
}
public function setRateExported(float $recordRate): void
{
$this->recordRateExported = $recordRate;
}
public function jsonSerialize() public function jsonSerialize()
{ {
return [ return [
'duration' => $this->recordDuration, 'duration' => $this->recordDuration,
'duration_billable' => $this->recordDurationBillable, 'duration_billable' => $this->recordDurationBillable,
'duration_exported' => $this->recordDurationExported,
'duration_billable_exported' => $this->recordDurationBillableExported,
'rate' => $this->recordRate, 'rate' => $this->recordRate,
'rate_billable' => $this->recordRateBillable, 'rate_billable' => $this->recordRateBillable,
'rate_exported' => $this->recordRateExported,
'rate_billable_exported' => $this->recordRateBillableExported,
'rate_internal' => $this->recordInternalRate, 'rate_internal' => $this->recordInternalRate,
'rate_internal_exported' => $this->internalRateExported,
'amount' => $this->counter, 'amount' => $this->counter,
'amount_billable' => $this->counterBillable, 'amount_billable' => $this->counterBillable,
'amount_exported' => $this->counterExported,
]; ];
} }
} }

View File

@@ -328,10 +328,12 @@ class ProjectStatisticService
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate') ->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
->addSelect('COUNT(t.id) as counter') ->addSelect('COUNT(t.id) as counter')
->addSelect('t.billable as billable') ->addSelect('t.billable as billable')
->addSelect('t.exported as exported')
->andWhere($qb->expr()->in('t.project', ':project')) ->andWhere($qb->expr()->in('t.project', ':project'))
->andWhere($qb->expr()->isNotNull('t.end')) ->andWhere($qb->expr()->isNotNull('t.end'))
->groupBy('id') ->groupBy('id')
->addGroupBy('billable') ->addGroupBy('billable')
->addGroupBy('exported')
->setParameter('project', array_keys($statistics)) ->setParameter('project', array_keys($statistics))
; ;
@@ -359,10 +361,20 @@ class ProjectStatisticService
$statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']); $statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']);
$statistic->setCounter($statistic->getCounter() + $resultRow['counter']); $statistic->setCounter($statistic->getCounter() + $resultRow['counter']);
if ($resultRow['billable']) { if ($resultRow['billable']) {
$statistic->setDurationBillable($resultRow['duration']); $statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']);
$statistic->setRateBillable($resultRow['rate']); $statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']);
$statistic->setInternalRateBillable($resultRow['internalRate']); $statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']);
$statistic->setCounterBillable($resultRow['counter']); $statistic->setCounterBillable($statistic->getCounterBillable() + $resultRow['counter']);
if ($resultRow['exported']) {
$statistic->setDurationBillableExported($statistic->getDurationBillableExported() + $resultRow['duration']);
$statistic->setRateBillableExported($statistic->getRateBillableExported() + $resultRow['rate']);
}
}
if ($resultRow['exported']) {
$statistic->setDurationExported($statistic->getDurationExported() + $resultRow['duration']);
$statistic->setRateExported($statistic->getRateExported() + $resultRow['rate']);
$statistic->setInternalRateExported($statistic->getInternalRateExported() + $resultRow['internalRate']);
$statistic->setCounterExported($statistic->getCounterExported() + $resultRow['counter']);
} }
} }
} }

View File

@@ -50,4 +50,9 @@ class BillableCalculator implements CalculatorInterface
break; break;
} }
} }
public function getPriority(): int
{
return 100;
}
} }

View File

@@ -42,4 +42,9 @@ final class DurationCalculator implements CalculatorInterface
$this->roundings->applyRoundings($record); $this->roundings->applyRoundings($record);
} }
public function getPriority(): int
{
return 200;
}
} }

View File

@@ -44,4 +44,9 @@ class RateCalculator implements CalculatorInterface
$record->setFixedRate($rate->getFixedRate()); $record->setFixedRate($rate->getFixedRate());
} }
} }
public function getPriority(): int
{
return 300;
}
} }

View File

@@ -22,6 +22,18 @@ interface CalculatorInterface
* The methods return value will not be evaluated. * The methods return value will not be evaluated.
* *
* @param Timesheet $record * @param Timesheet $record
* @ param array<string, array<mixed, mixed>> $changeset
* @return void
*/ */
public function calculate(Timesheet $record); public function calculate(Timesheet $record/*, array $changeset*/);
/*
* FIXME use with Kimai 2.0
*
* Default priority is 1000 (after all system Calculator were executed).
* The higher the priority the later it will be executed.
*
* @return int
*/
//public function getPriority(): int;
} }

View File

@@ -23,7 +23,10 @@
{% endif %} {% endif %}
{% set columns = columns|merge({'duration': {'class': 'text-right'}}) %} {% set columns = columns|merge({'duration': {'class': 'text-right'}}) %}
{% if canSeeRate %} {% if canSeeRate %}
{% set columns = columns|merge({'rate': {'class': 'text-right'}}) %} {% set columns = columns|merge({
'hourlyRate': {'class': 'text-right hidden'},
'rate': {'class': 'text-right'}
}) %}
{% endif %} {% endif %}
{% set columns = columns|merge({ {% set columns = columns|merge({
'customer': {'class': 'hidden-xs hidden-sm hidden-md'}, 'customer': {'class': 'hidden-xs hidden-sm hidden-md'},
@@ -71,17 +74,20 @@
{% set day = null %} {% set day = null %}
{% set dayDuration = 0 %} {% set dayDuration = 0 %}
{% set dayRate = {} %} {% set dayRate = {} %}
{% set dayHourlyRate = 0 %}
{% set lastEntry = null %} {% set lastEntry = null %}
{% for entry in entries %} {% for entry in entries %}
{%- set customerCurrency = entry.project.customer.currency -%} {%- set customerCurrency = entry.project.customer.currency -%}
{%- set entryHourlyRate = entry.hourlyRate|money(customerCurrency) -%}
{%- if day is same as(null) -%} {%- if day is same as(null) -%}
{% set day = entry.begin|date_short %} {% set day = entry.begin|date_short %}
{% endif %} {% endif %}
{%- if showSummary and day is not same as(entry.begin|date_short) -%} {%- if showSummary and day is not same as(entry.begin|date_short) -%}
{{ _self.summary(day, dayDuration, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }} {{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }}
{% set day = entry.begin|date_short %} {% set day = entry.begin|date_short %}
{% set dayDuration = 0 %} {% set dayDuration = 0 %}
{% set dayRate = {} %} {% set dayRate = {} %}
{% set dayHourlyRate = 0 %}
{%- endif -%} {%- endif -%}
{% set class = '' %} {% set class = '' %}
{% if checkOverlappingDesc or checkOverlappingAsc %} {% if checkOverlappingDesc or checkOverlappingAsc %}
@@ -124,11 +130,14 @@
{% endif %} {% endif %}
{% if canSeeRate %} {% if canSeeRate %}
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'hourlyRate') }}">
{{ entryHourlyRate }}
</td>
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}"> <td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">
{% if not entry.end or not is_granted('view_rate', entry) %} {% if not entry.end or not is_granted('view_rate', entry) %}
&dash; &dash;
{% else %} {% else %}
{{ entry.rate|money(entry.project.customer.currency) }} {{ entry.rate|money(customerCurrency) }}
{% endif %} {% endif %}
</td> </td>
{% endif %} {% endif %}
@@ -179,12 +188,19 @@
{% endif %} {% endif %}
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %} {% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
{%- endif -%} {%- endif -%}
{% if dayHourlyRate is not null %}
{% if dayHourlyRate == 0 %}
{% set dayHourlyRate = entryHourlyRate %}
{% elseif dayHourlyRate != entryHourlyRate %}
{% set dayHourlyRate = null %}
{% endif %}
{% endif %}
{%- set dayDuration = dayDuration + entry.duration -%} {%- set dayDuration = dayDuration + entry.duration -%}
{% set lastEntry = entry %} {% set lastEntry = entry %}
{% endfor %} {% endfor %}
{% if showSummary %} {% if showSummary %}
{{ _self.summary(day, dayDuration, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }} {{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }}
{% endif %} {% endif %}
{{ tables.data_table_footer(entries, paginationRoute, multiUpdateForm) }} {{ tables.data_table_footer(entries, paginationRoute, multiUpdateForm) }}
@@ -192,7 +208,7 @@
{% endblock %} {% endblock %}
{% macro summary(day, duration, dayRates, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) %} {% macro summary(day, duration, dayHourlyRate, dayRates, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
<tr class="summary info"> <tr class="summary info">
<td></td> <td></td>
@@ -203,6 +219,11 @@
{% endif %} {% endif %}
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ duration|duration }}</td> <td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ duration|duration }}</td>
{% if canSeeRate %} {% if canSeeRate %}
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'hourlyRate') }}">
{% if dayHourlyRate is not null and dayHourlyRate != 0 %}
{{ dayHourlyRate }}
{% endif %}
</td>
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}"> <td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">
{% for currency, rate in dayRates %} {% for currency, rate in dayRates %}
{{ rate|money(currency) }} {{ rate|money(currency) }}

View File

@@ -157,6 +157,26 @@ class TimesheetTest extends TestCase
self::assertTrue($sut->isBillable()); self::assertTrue($sut->isBillable());
} }
public function testResetRates()
{
$sut = new Timesheet();
self::assertSame(0.00, $sut->getRate());
self::assertNull($sut->getFixedRate());
self::assertNull($sut->getInternalRate());
self::assertNull($sut->getHourlyRate());
$sut->setRate(123.45);
$sut->setFixedRate(42.32);
$sut->setInternalRate(212);
$sut->setHourlyRate(123);
$sut->resetRates();
self::assertSame(0.00, $sut->getRate());
self::assertNull($sut->getFixedRate());
self::assertNull($sut->getInternalRate());
self::assertNull($sut->getHourlyRate());
}
public function testCategory() public function testCategory()
{ {
$sut = new Timesheet(); $sut = new Timesheet();

View File

@@ -28,17 +28,25 @@ abstract class AbstractTimesheetCountedStatisticTest extends TestCase
self::assertSame(0.0, $sut->getRateBillable()); self::assertSame(0.0, $sut->getRateBillable());
self::assertSame(0, $sut->getRecordAmountBillable()); self::assertSame(0, $sut->getRecordAmountBillable());
self::assertSame(0.0, $sut->getInternalRateBillable()); self::assertSame(0.0, $sut->getInternalRateBillable());
self::assertSame(0, $sut->getDurationBillableExported());
self::assertSame(0, $sut->getDurationExported());
self::assertSame(0.0, $sut->getRateExported());
self::assertSame(0.0, $sut->getInternalRateExported());
$json = $sut->jsonSerialize(); $json = $sut->jsonSerialize();
$expected = [ $expected = [
'duration' => 0, 'duration' => 0,
'duration_billable' => 0, 'duration_billable' => 0,
'duration_exported' => 0,
'duration_billable_exported' => 0,
'rate' => 0.0, 'rate' => 0.0,
'rate_billable' => 0.0, 'rate_billable' => 0.0,
'rate_exported' => 0.0,
'rate_internal' => 0.0, 'rate_internal' => 0.0,
'amount' => 0, 'amount' => 0,
'amount_billable' => 0, 'amount_billable' => 0,
'amount_exported' => 0,
]; ];
foreach ($expected as $key => $value) { foreach ($expected as $key => $value) {
@@ -55,12 +63,22 @@ abstract class AbstractTimesheetCountedStatisticTest extends TestCase
$sut->setRecordAmountBillable(15); $sut->setRecordAmountBillable(15);
self::assertInstanceOf(TimesheetCountedStatistic::class, $sut->setRecordInternalRate(99.09)); self::assertInstanceOf(TimesheetCountedStatistic::class, $sut->setRecordInternalRate(99.09));
$sut->setDurationBillableExported(199);
$sut->setDurationExported(299);
$sut->setRateExported(456.48);
$sut->setInternalRateExported(27.15);
self::assertSame(23.97, $sut->getRecordRate()); self::assertSame(23.97, $sut->getRecordRate());
self::assertSame(21, $sut->getRecordDuration()); self::assertSame(21, $sut->getRecordDuration());
self::assertSame(5, $sut->getRecordAmount()); self::assertSame(5, $sut->getRecordAmount());
self::assertSame(15, $sut->getRecordAmountBillable()); self::assertSame(15, $sut->getRecordAmountBillable());
self::assertSame(99.09, $sut->getRecordInternalRate()); self::assertSame(99.09, $sut->getRecordInternalRate());
self::assertSame(199, $sut->getDurationBillableExported());
self::assertSame(299, $sut->getDurationExported());
self::assertSame(456.48, $sut->getRateExported());
self::assertSame(27.15, $sut->getInternalRateExported());
self::assertSame(21, $sut->getValue()); self::assertSame(21, $sut->getValue());
$sut->setCounter(524); $sut->setCounter(524);
@@ -93,15 +111,24 @@ abstract class AbstractTimesheetCountedStatisticTest extends TestCase
$sut->setDurationBillable(1234); $sut->setDurationBillable(1234);
$sut->setRecordAmountBillable(4321); $sut->setRecordAmountBillable(4321);
$sut->setDurationBillableExported(199);
$sut->setRateBillableExported(654.23);
$sut->setDurationExported(299);
$sut->setRateExported(456.48);
$sut->setInternalRateExported(27.15);
$json = $sut->jsonSerialize(); $json = $sut->jsonSerialize();
foreach (['duration', 'duration_billable', 'rate', 'rate_billable', 'rate_internal', 'amount', 'amount_billable'] as $key) { foreach (['duration', 'duration_billable', 'duration_exported', 'rate', 'rate_billable', 'rate_billable_exported', 'rate_exported', 'rate_internal', 'amount', 'amount_billable', 'amount_exported'] as $key) {
self::assertArrayHasKey($key, $json); self::assertArrayHasKey($key, $json);
} }
self::assertSame(21, $json['duration']); self::assertSame(21, $json['duration']);
self::assertSame(1234, $json['duration_billable']); self::assertSame(1234, $json['duration_billable']);
self::assertSame(199, $json['duration_billable_exported']);
self::assertSame(299, $json['duration_exported']);
self::assertSame(23.97, $json['rate']); self::assertSame(23.97, $json['rate']);
self::assertSame(123.456, $json['rate_billable']); self::assertSame(123.456, $json['rate_billable']);
self::assertSame(654.23, $json['rate_billable_exported']);
self::assertSame(99.09, $json['rate_internal']); self::assertSame(99.09, $json['rate_internal']);
self::assertSame(5, $json['amount']); self::assertSame(5, $json['amount']);
self::assertSame(4321, $json['amount_billable']); self::assertSame(4321, $json['amount_billable']);