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:
@@ -149,10 +149,20 @@ class ActivityStatisticService
|
||||
$statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']);
|
||||
$statistic->setCounter($statistic->getCounter() + $resultRow['counter']);
|
||||
if ($resultRow['billable']) {
|
||||
$statistic->setDurationBillable($resultRow['duration']);
|
||||
$statistic->setRateBillable($resultRow['rate']);
|
||||
$statistic->setInternalRateBillable($resultRow['internalRate']);
|
||||
$statistic->setCounterBillable($resultRow['counter']);
|
||||
$statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']);
|
||||
$statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']);
|
||||
$statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']);
|
||||
$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('COUNT(t.id) as counter')
|
||||
->addSelect('t.billable as billable')
|
||||
->addSelect('t.exported as exported')
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->groupBy('id')
|
||||
->addGroupBy('billable')
|
||||
->addGroupBy('exported')
|
||||
->andWhere($qb->expr()->in('t.activity', ':activity'))
|
||||
->setParameter('activity', $activities)
|
||||
;
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '1.20';
|
||||
public const VERSION = '1.20.1';
|
||||
/**
|
||||
* 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"
|
||||
*/
|
||||
|
||||
@@ -97,10 +97,20 @@ class CustomerStatisticService
|
||||
$statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']);
|
||||
$statistic->setCounter($statistic->getCounter() + $resultRow['counter']);
|
||||
if ($resultRow['billable']) {
|
||||
$statistic->setDurationBillable($resultRow['duration']);
|
||||
$statistic->setRateBillable($resultRow['rate']);
|
||||
$statistic->setInternalRateBillable($resultRow['internalRate']);
|
||||
$statistic->setCounterBillable($resultRow['counter']);
|
||||
$statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']);
|
||||
$statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']);
|
||||
$statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']);
|
||||
$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('COUNT(t.id) as counter')
|
||||
->addSelect('t.billable as billable')
|
||||
->addSelect('t.exported as exported')
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->groupBy('id')
|
||||
->addGroupBy('billable')
|
||||
->addGroupBy('exported')
|
||||
->andWhere($qb->expr()->in('p.customer', ':customer'))
|
||||
->setParameter('customer', $customers)
|
||||
;
|
||||
|
||||
@@ -23,7 +23,11 @@ class TimesheetSubscriber implements EventSubscriber
|
||||
/**
|
||||
* @var CalculatorInterface[]
|
||||
*/
|
||||
protected $calculator;
|
||||
private $calculator;
|
||||
/**
|
||||
* @var CalculatorInterface[]
|
||||
*/
|
||||
private $sorted;
|
||||
|
||||
/**
|
||||
* @param CalculatorInterface[] $calculators
|
||||
@@ -33,20 +37,14 @@ class TimesheetSubscriber implements EventSubscriber
|
||||
$this->calculator = $calculators;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSubscribedEvents()
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
Events::onFlush,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OnFlushEventArgs $args
|
||||
*/
|
||||
public function onFlush(OnFlushEventArgs $args)
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$em = $args->getEntityManager();
|
||||
$uow = $em->getUnitOfWork();
|
||||
@@ -57,7 +55,7 @@ class TimesheetSubscriber implements EventSubscriber
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->calculateFields($entity);
|
||||
$this->calculateFields($entity, $uow->getEntityChangeSet($entity));
|
||||
$uow->recomputeSingleEntityChangeSet($meta, $entity);
|
||||
}
|
||||
|
||||
@@ -71,13 +69,31 @@ class TimesheetSubscriber implements EventSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entity
|
||||
*/
|
||||
protected function calculateFields(Timesheet $entity)
|
||||
protected function calculateFields(Timesheet $entity, array $changes = []): void
|
||||
{
|
||||
foreach ($this->calculator as $calculator) {
|
||||
$calculator->calculate($entity);
|
||||
if ($this->sorted === null) {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,6 +654,14 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resetRates(): void
|
||||
{
|
||||
$this->rate = 0.00;
|
||||
$this->internalRate = null;
|
||||
$this->hourlyRate = null;
|
||||
$this->fixedRate = null;
|
||||
}
|
||||
|
||||
public function isBillable(): bool
|
||||
{
|
||||
return $this->billable;
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Export\Base;
|
||||
|
||||
use App\Activity\ActivityStatisticService;
|
||||
use App\Invoice\InvoiceItemInterface;
|
||||
use App\Model\TimesheetCountedStatistic;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
|
||||
@@ -172,6 +173,7 @@ trait RendererTrait
|
||||
{
|
||||
$summary = [];
|
||||
$projects = [];
|
||||
$empty = new TimesheetCountedStatistic();
|
||||
|
||||
foreach ($exportItems as $exportItem) {
|
||||
$customer = null;
|
||||
@@ -183,15 +185,14 @@ trait RendererTrait
|
||||
$customer = $project->getCustomer();
|
||||
$customerId = $customer->getId();
|
||||
$projectId = $project->getId();
|
||||
if ($project->hasBudgets()) {
|
||||
$projects[] = $project;
|
||||
}
|
||||
$projects[] = $project;
|
||||
}
|
||||
|
||||
$id = $customerId . '_' . $projectId;
|
||||
|
||||
if (!isset($summary[$id])) {
|
||||
$summary[$id] = [
|
||||
'totals' => $empty->jsonSerialize(),
|
||||
'time' => $project->getTimeBudget(),
|
||||
'money' => $project->getBudget(),
|
||||
'time_left' => null,
|
||||
@@ -209,6 +210,8 @@ trait RendererTrait
|
||||
foreach ($allBudgets as $projectId => $statisticModel) {
|
||||
$project = $statisticModel->getProject();
|
||||
$id = $project->getCustomer()->getId() . '_' . $projectId;
|
||||
$total = $statisticModel->getStatisticTotal();
|
||||
$summary[$id]['totals'] = $total->jsonSerialize();
|
||||
if ($statisticModel->hasTimeBudget()) {
|
||||
$summary[$id]['time_left'] = $statisticModel->getTimeBudgetOpenRelative();
|
||||
$summary[$id]['time_left_total'] = $statisticModel->getTimeBudgetOpen();
|
||||
@@ -255,6 +258,7 @@ trait RendererTrait
|
||||
{
|
||||
$summary = [];
|
||||
$activities = [];
|
||||
$empty = new TimesheetCountedStatistic();
|
||||
|
||||
foreach ($exportItems as $exportItem) {
|
||||
$customerId = 'none';
|
||||
@@ -270,9 +274,7 @@ trait RendererTrait
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($activity->hasBudgets()) {
|
||||
$activities[] = $activity;
|
||||
}
|
||||
$activities[] = $activity;
|
||||
|
||||
if (null !== ($project = $exportItem->getProject())) {
|
||||
$projectId = $project->getId();
|
||||
@@ -289,6 +291,7 @@ trait RendererTrait
|
||||
|
||||
if (!isset($summary[$id][$activityId])) {
|
||||
$summary[$id][$activityId] = [
|
||||
'totals' => $empty->jsonSerialize(),
|
||||
'time' => $activity->getTimeBudget(),
|
||||
'money' => $activity->getBudget(),
|
||||
'time_left' => null,
|
||||
@@ -306,6 +309,8 @@ trait RendererTrait
|
||||
foreach ($allBudgets as $activityId => $statisticModel) {
|
||||
$project = $statisticModel->getActivity()->getProject();
|
||||
$id = $project->getCustomer()->getId() . '_' . $project->getId();
|
||||
$total = $statisticModel->getStatisticTotal();
|
||||
$summary[$id][$activityId]['totals'] = $total->jsonSerialize();
|
||||
if ($statisticModel->hasTimeBudget()) {
|
||||
$summary[$id][$activityId]['time_left'] = $statisticModel->getTimeBudgetOpenRelative();
|
||||
$summary[$id][$activityId]['time_left_total'] = $statisticModel->getTimeBudgetOpen();
|
||||
|
||||
@@ -21,6 +21,14 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
private $recordRateBillable = 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.
|
||||
*
|
||||
@@ -46,6 +54,16 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
$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.
|
||||
*
|
||||
@@ -178,6 +196,16 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
$this->internalRateBillable = $internalRateBillable;
|
||||
}
|
||||
|
||||
public function getInternalRateExported(): float
|
||||
{
|
||||
return $this->internalRateExported;
|
||||
}
|
||||
|
||||
public function setInternalRateExported(float $internalRateExported): void
|
||||
{
|
||||
$this->internalRateExported = $internalRateExported;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $recordInternalRate
|
||||
* @return $this
|
||||
@@ -217,6 +245,16 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
$this->recordDurationBillable = $recordDuration;
|
||||
}
|
||||
|
||||
public function getDurationBillableExported(): int
|
||||
{
|
||||
return $this->recordDurationBillableExported;
|
||||
}
|
||||
|
||||
public function setDurationBillableExported(int $recordDuration): void
|
||||
{
|
||||
$this->recordDurationBillableExported = $recordDuration;
|
||||
}
|
||||
|
||||
public function getRateBillable(): float
|
||||
{
|
||||
return $this->recordRateBillable;
|
||||
@@ -227,16 +265,52 @@ class TimesheetCountedStatistic implements \JsonSerializable
|
||||
$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()
|
||||
{
|
||||
return [
|
||||
'duration' => $this->recordDuration,
|
||||
'duration_billable' => $this->recordDurationBillable,
|
||||
'duration_exported' => $this->recordDurationExported,
|
||||
'duration_billable_exported' => $this->recordDurationBillableExported,
|
||||
'rate' => $this->recordRate,
|
||||
'rate_billable' => $this->recordRateBillable,
|
||||
'rate_exported' => $this->recordRateExported,
|
||||
'rate_billable_exported' => $this->recordRateBillableExported,
|
||||
'rate_internal' => $this->recordInternalRate,
|
||||
'rate_internal_exported' => $this->internalRateExported,
|
||||
'amount' => $this->counter,
|
||||
'amount_billable' => $this->counterBillable,
|
||||
'amount_exported' => $this->counterExported,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,10 +328,12 @@ class ProjectStatisticService
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
|
||||
->addSelect('COUNT(t.id) as counter')
|
||||
->addSelect('t.billable as billable')
|
||||
->addSelect('t.exported as exported')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->groupBy('id')
|
||||
->addGroupBy('billable')
|
||||
->addGroupBy('exported')
|
||||
->setParameter('project', array_keys($statistics))
|
||||
;
|
||||
|
||||
@@ -359,10 +361,20 @@ class ProjectStatisticService
|
||||
$statistic->setInternalRate($statistic->getInternalRate() + $resultRow['internalRate']);
|
||||
$statistic->setCounter($statistic->getCounter() + $resultRow['counter']);
|
||||
if ($resultRow['billable']) {
|
||||
$statistic->setDurationBillable($resultRow['duration']);
|
||||
$statistic->setRateBillable($resultRow['rate']);
|
||||
$statistic->setInternalRateBillable($resultRow['internalRate']);
|
||||
$statistic->setCounterBillable($resultRow['counter']);
|
||||
$statistic->setDurationBillable($statistic->getDurationBillable() + $resultRow['duration']);
|
||||
$statistic->setRateBillable($statistic->getRateBillable() + $resultRow['rate']);
|
||||
$statistic->setInternalRateBillable($statistic->getInternalRateBillable() + $resultRow['internalRate']);
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,4 +50,9 @@ class BillableCalculator implements CalculatorInterface
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,4 +42,9 @@ final class DurationCalculator implements CalculatorInterface
|
||||
|
||||
$this->roundings->applyRoundings($record);
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,9 @@ class RateCalculator implements CalculatorInterface
|
||||
$record->setFixedRate($rate->getFixedRate());
|
||||
}
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 300;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,18 @@ interface CalculatorInterface
|
||||
* The methods return value will not be evaluated.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({'duration': {'class': 'text-right'}}) %}
|
||||
{% if canSeeRate %}
|
||||
{% set columns = columns|merge({'rate': {'class': 'text-right'}}) %}
|
||||
{% set columns = columns|merge({
|
||||
'hourlyRate': {'class': 'text-right hidden'},
|
||||
'rate': {'class': 'text-right'}
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({
|
||||
'customer': {'class': 'hidden-xs hidden-sm hidden-md'},
|
||||
@@ -71,17 +74,20 @@
|
||||
{% set day = null %}
|
||||
{% set dayDuration = 0 %}
|
||||
{% set dayRate = {} %}
|
||||
{% set dayHourlyRate = 0 %}
|
||||
{% set lastEntry = null %}
|
||||
{% for entry in entries %}
|
||||
{%- set customerCurrency = entry.project.customer.currency -%}
|
||||
{%- set entryHourlyRate = entry.hourlyRate|money(customerCurrency) -%}
|
||||
{%- if day is same as(null) -%}
|
||||
{% set day = entry.begin|date_short %}
|
||||
{% endif %}
|
||||
{%- 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 dayDuration = 0 %}
|
||||
{% set dayRate = {} %}
|
||||
{% set dayHourlyRate = 0 %}
|
||||
{%- endif -%}
|
||||
{% set class = '' %}
|
||||
{% if checkOverlappingDesc or checkOverlappingAsc %}
|
||||
@@ -124,11 +130,14 @@
|
||||
{% endif %}
|
||||
|
||||
{% 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') }}">
|
||||
{% if not entry.end or not is_granted('view_rate', entry) %}
|
||||
‐
|
||||
{% else %}
|
||||
{{ entry.rate|money(entry.project.customer.currency) }}
|
||||
{{ entry.rate|money(customerCurrency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
@@ -179,12 +188,19 @@
|
||||
{% endif %}
|
||||
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
|
||||
{%- 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 lastEntry = entry %}
|
||||
{% endfor %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{{ tables.data_table_footer(entries, paginationRoute, multiUpdateForm) }}
|
||||
@@ -192,7 +208,7 @@
|
||||
|
||||
{% 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 %}
|
||||
<tr class="summary info">
|
||||
<td></td>
|
||||
@@ -203,6 +219,11 @@
|
||||
{% endif %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ duration|duration }}</td>
|
||||
{% 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') }}">
|
||||
{% for currency, rate in dayRates %}
|
||||
{{ rate|money(currency) }}
|
||||
|
||||
@@ -157,6 +157,26 @@ class TimesheetTest extends TestCase
|
||||
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()
|
||||
{
|
||||
$sut = new Timesheet();
|
||||
|
||||
@@ -28,17 +28,25 @@ abstract class AbstractTimesheetCountedStatisticTest extends TestCase
|
||||
self::assertSame(0.0, $sut->getRateBillable());
|
||||
self::assertSame(0, $sut->getRecordAmountBillable());
|
||||
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();
|
||||
|
||||
$expected = [
|
||||
'duration' => 0,
|
||||
'duration_billable' => 0,
|
||||
'duration_exported' => 0,
|
||||
'duration_billable_exported' => 0,
|
||||
'rate' => 0.0,
|
||||
'rate_billable' => 0.0,
|
||||
'rate_exported' => 0.0,
|
||||
'rate_internal' => 0.0,
|
||||
'amount' => 0,
|
||||
'amount_billable' => 0,
|
||||
'amount_exported' => 0,
|
||||
];
|
||||
|
||||
foreach ($expected as $key => $value) {
|
||||
@@ -55,12 +63,22 @@ abstract class AbstractTimesheetCountedStatisticTest extends TestCase
|
||||
$sut->setRecordAmountBillable(15);
|
||||
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(21, $sut->getRecordDuration());
|
||||
self::assertSame(5, $sut->getRecordAmount());
|
||||
self::assertSame(15, $sut->getRecordAmountBillable());
|
||||
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());
|
||||
|
||||
$sut->setCounter(524);
|
||||
@@ -93,15 +111,24 @@ abstract class AbstractTimesheetCountedStatisticTest extends TestCase
|
||||
$sut->setDurationBillable(1234);
|
||||
$sut->setRecordAmountBillable(4321);
|
||||
|
||||
$sut->setDurationBillableExported(199);
|
||||
$sut->setRateBillableExported(654.23);
|
||||
$sut->setDurationExported(299);
|
||||
$sut->setRateExported(456.48);
|
||||
$sut->setInternalRateExported(27.15);
|
||||
|
||||
$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::assertSame(21, $json['duration']);
|
||||
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(123.456, $json['rate_billable']);
|
||||
self::assertSame(654.23, $json['rate_billable_exported']);
|
||||
self::assertSame(99.09, $json['rate_internal']);
|
||||
self::assertSame(5, $json['amount']);
|
||||
self::assertSame(4321, $json['amount_billable']);
|
||||
|
||||
Reference in New Issue
Block a user