Configurable rate rounding (#5734)
* added invoice hydration of the issuer object * added "rate calculator" mode * new config to select rounding mode * replace static calls with dependency injection
This commit is contained in:
@@ -86,6 +86,9 @@ services:
|
||||
App\Timesheet\RateService:
|
||||
arguments: ['%kimai.timesheet.rates%']
|
||||
|
||||
App\Timesheet\RateCalculator\RateCalculatorMode:
|
||||
factory: ['@App\Timesheet\RateCalculator\RateCalculatorFactory', getRateCalculatorMode]
|
||||
|
||||
# ================================================================================
|
||||
# SECURITY & VOTER
|
||||
# ================================================================================
|
||||
|
||||
@@ -969,11 +969,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:createTimesheetEntry\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/DataFixtures/TimesheetFixtures.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:createPermissionParameter\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -449,6 +449,11 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setType(TextType::class)
|
||||
->setConstraints([new NotBlank()])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration('invoice.rounding_mode'))
|
||||
->setLabel('invoice.rounding_mode')
|
||||
->setType(ChoiceType::class)
|
||||
->setOptions(['choices' => ['classic' => 'classic', 'decimal' => 'decimal']])
|
||||
->setTranslationDomain('system-configuration'),
|
||||
]),
|
||||
$authentication,
|
||||
(new SystemConfigurationModel('customer'))
|
||||
|
||||
@@ -15,7 +15,8 @@ use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\Util;
|
||||
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -60,10 +61,14 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
||||
$allUser = $this->getAllUsers($manager);
|
||||
$faker = Factory::create();
|
||||
$all = 0;
|
||||
$calculator = new ClassicRateCalculator();
|
||||
|
||||
foreach ($allUser as $user) {
|
||||
// reload, because the manager might have been cleared
|
||||
$user = $manager->find(User::class, $user->getId());
|
||||
if ($user === null) {
|
||||
continue;
|
||||
}
|
||||
// random amount of timesheet entries for every user
|
||||
$timesheetForUser = rand(self::MIN_TIMESHEETS_PER_USER, self::MAX_TIMESHEETS_PER_USER);
|
||||
|
||||
@@ -84,6 +89,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
||||
}
|
||||
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$calculator,
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
@@ -98,6 +104,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
||||
// create active records
|
||||
if ($all % 3 === 0) {
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$calculator,
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
@@ -208,7 +215,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
||||
return $this->findRandom($manager, Activity::class, 50);
|
||||
}
|
||||
|
||||
private function createTimesheetEntry(User $user, Activity $activity, Project $project, ?string $description, bool $setEndDate): Timesheet
|
||||
private function createTimesheetEntry(RateCalculatorMode $calculatorMode, User $user, Activity $activity, Project $project, ?string $description, bool $setEndDate): Timesheet
|
||||
{
|
||||
$start = $this->getRandomFirstDay();
|
||||
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');
|
||||
@@ -227,7 +234,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
||||
|
||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||
$hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
$rate = Util::calculateRate($hourlyRate, $duration);
|
||||
$rate = $calculatorMode->calculateRate($hourlyRate, $duration);
|
||||
|
||||
$entry->setEnd($end);
|
||||
$entry->setRate($rate);
|
||||
|
||||
@@ -369,6 +369,10 @@ final class Configuration implements ConfigurationInterface
|
||||
->booleanNode('upload_twig')
|
||||
->defaultFalse()
|
||||
->end()
|
||||
->enumNode('rounding_mode')
|
||||
->values(['decimal', 'classic'])
|
||||
->defaultValue('classic')
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace App\Invoice\Calculator;
|
||||
use App\Invoice\InvoiceItem;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\TaxRow;
|
||||
use App\Timesheet\Util;
|
||||
|
||||
abstract class AbstractCalculator
|
||||
{
|
||||
@@ -42,10 +41,10 @@ abstract class AbstractCalculator
|
||||
if (\count($this->cached) === 0) {
|
||||
foreach ($this->calculateEntries() as $entry) {
|
||||
if (!$entry->isFixedRate() && $entry->getHourlyRate() !== null && $entry->getHourlyRate() > 0) {
|
||||
$entry->setDuration(Util::decimalizeDuration($entry->getDuration()));
|
||||
$entry->setDuration($this->model->getRateCalculatorMode()->roundDuration($entry->getDuration()));
|
||||
// when merging many entries, we might run into rounding issues
|
||||
// so we have to recalculate the hourly rate here
|
||||
$entry->setRate(Util::calculateRate($entry->getHourlyRate(), $entry->getDuration()));
|
||||
$entry->setRate($this->model->getRateCalculatorMode()->calculateRate($entry->getHourlyRate(), $entry->getDuration()));
|
||||
}
|
||||
|
||||
$this->cached[] = $entry;
|
||||
|
||||
@@ -32,7 +32,7 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
|
||||
|
||||
$values = [
|
||||
$prefix . 'id' => $customer->getId(),
|
||||
$prefix . 'address' => $customer->getFormattedAddress() ?? '',
|
||||
$prefix . 'address' => $customer->getFormattedAddress() ?? '', // deprecated since 2.44
|
||||
$prefix . 'address_line1' => $customer->getAddressLine1() ?? '',
|
||||
$prefix . 'address_line2' => $customer->getAddressLine2() ?? '',
|
||||
$prefix . 'address_line3' => $customer->getAddressLine3() ?? '',
|
||||
|
||||
@@ -62,12 +62,12 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
||||
'invoice.subtotal_plain' => $subtotal,
|
||||
|
||||
'template.name' => $template->getName() ?? '',
|
||||
'template.company' => $template->getCompany() ?? '',
|
||||
'template.company' => $template->getCompany() ?? '', // deprecated since 2.45
|
||||
'template.address' => $template->getAddress() ?? '',
|
||||
'template.title' => $template->getTitle() ?? '',
|
||||
'template.payment_terms' => $template->getPaymentTerms() ?? '',
|
||||
'template.due_days' => $template->getDueDays(),
|
||||
'template.vat_id' => $template->getVatId() ?? '',
|
||||
'template.vat_id' => $template->getVatId() ?? '', // deprecated since 2.45
|
||||
'template.contact' => $template->getContact() ?? '',
|
||||
'template.country' => null,
|
||||
'template.country_name' => null,
|
||||
@@ -75,16 +75,16 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
||||
|
||||
'query.begin' => '',
|
||||
'query.begin_day' => '',
|
||||
'query.begin_process' => null, // since 2.14
|
||||
'query.begin_process' => null, // since 2.14
|
||||
'query.begin_month' => '',
|
||||
'query.begin_month_number' => '',
|
||||
'query.begin_year' => '',
|
||||
'query.end' => '', // since 1.9
|
||||
'query.end_day' => '', // since 1.9
|
||||
'query.end_process' => null, // since 2.14
|
||||
'query.end_month' => '', // since 1.9
|
||||
'query.end_month_number' => '', // since 1.9
|
||||
'query.end_year' => '', // since 1.9
|
||||
'query.end' => '', // since 1.9
|
||||
'query.end_day' => '', // since 1.9
|
||||
'query.end_process' => null, // since 2.14
|
||||
'query.end_month' => '', // since 1.9
|
||||
'query.end_month_number' => '', // since 1.9
|
||||
'query.end_year' => '', // since 1.9
|
||||
|
||||
// since 2.0.15
|
||||
'user.see_others' => ($model->getQuery()?->getUser() === null),
|
||||
@@ -105,8 +105,8 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
||||
if ($seller !== null) {
|
||||
$country = $seller->getCountry();
|
||||
if ($country !== null) {
|
||||
$values['template.country'] = $country;
|
||||
$values['template.country_name'] = Countries::getName($country, $language);
|
||||
$values['template.country'] = $country; // deprecated since 2.45
|
||||
$values['template.country_name'] = Countries::getName($country, $language); // deprecated since 2.45
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +116,10 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
||||
if ($begin !== null) {
|
||||
$values = array_merge($values, [
|
||||
'query.day' => $begin->format('d'),
|
||||
// @deprecated - but impossible to delete
|
||||
'query.month' => $formatter->getFormattedMonthName($begin),
|
||||
// @deprecated - but impossible to delete
|
||||
'query.month_number' => $begin->format('m'),
|
||||
// @deprecated - but impossible to delete
|
||||
'query.year' => $begin->format('Y'),
|
||||
// @deprecated - but impossible to delete
|
||||
'query.begin' => $formatter->getFormattedDateTime($begin),
|
||||
'query.month' => $formatter->getFormattedMonthName($begin), // @deprecated - but impossible to delete
|
||||
'query.month_number' => $begin->format('m'), // @deprecated - but impossible to delete
|
||||
'query.year' => $begin->format('Y'), // @deprecated - but impossible to delete
|
||||
'query.begin' => $formatter->getFormattedDateTime($begin), // @deprecated - but impossible to delete
|
||||
'query.begin_process' => $begin->format(self::DATE_PROCESS_FORMAT), // since 2.14
|
||||
'query.begin_day' => $begin->format('d'),
|
||||
'query.begin_month' => $formatter->getFormattedMonthName($begin),
|
||||
|
||||
65
src/Invoice/Hydrator/InvoiceModelIssuerHydrator.php
Normal file
65
src/Invoice/Hydrator/InvoiceModelIssuerHydrator.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceModelHydrator;
|
||||
use Symfony\Component\Intl\Countries;
|
||||
|
||||
final class InvoiceModelIssuerHydrator implements InvoiceModelHydrator
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function hydrate(InvoiceModel $model): array
|
||||
{
|
||||
$customer = $model->getTemplate()->getCustomer();
|
||||
if (null === $customer) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$prefix = 'issuer.';
|
||||
$language = $model->getTemplate()->getLanguage();
|
||||
$country = $customer->getCountry();
|
||||
|
||||
$values = [
|
||||
$prefix . 'id' => $customer->getId(),
|
||||
$prefix . 'address' => $customer->getFormattedAddress() ?? '',
|
||||
$prefix . 'address_line1' => $customer->getAddressLine1() ?? '',
|
||||
$prefix . 'address_line2' => $customer->getAddressLine2() ?? '',
|
||||
$prefix . 'address_line3' => $customer->getAddressLine3() ?? '',
|
||||
$prefix . 'postcode' => $customer->getPostCode() ?? '',
|
||||
$prefix . 'city' => $customer->getCity() ?? '',
|
||||
$prefix . 'name' => $customer->getName() ?? '',
|
||||
$prefix . 'contact' => $customer->getContact() ?? '',
|
||||
$prefix . 'company' => $customer->getCompany() ?? '',
|
||||
$prefix . 'vat_id' => $customer->getVatId() ?? '',
|
||||
$prefix . 'number' => $customer->getNumber() ?? '',
|
||||
$prefix . 'country' => $country,
|
||||
$prefix . 'country_name' => $country !== null ? Countries::getName($country, $language) : null,
|
||||
$prefix . 'homepage' => $customer->getHomepage() ?? '',
|
||||
$prefix . 'comment' => $customer->getComment() ?? '',
|
||||
$prefix . 'email' => $customer->getEmail() ?? '',
|
||||
$prefix . 'fax' => $customer->getFax() ?? '',
|
||||
$prefix . 'phone' => $customer->getPhone() ?? '',
|
||||
$prefix . 'mobile' => $customer->getMobile() ?? '',
|
||||
$prefix . 'invoice_text' => $customer->getInvoiceText() ?? '',
|
||||
$prefix . 'buyer_reference' => $customer->getBuyerReference() ?? '',
|
||||
];
|
||||
|
||||
foreach ($customer->getMetaFields() as $metaField) {
|
||||
$values = array_merge($values, [
|
||||
$prefix . 'meta.' . $metaField->getName() => $metaField->getValue(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,12 @@ use App\Invoice\Hydrator\InvoiceItemDefaultHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelActivityHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelCustomerHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelDefaultHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelIssuerHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelProjectHydrator;
|
||||
use App\Invoice\Hydrator\InvoiceModelUserHydrator;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||
|
||||
/**
|
||||
@@ -68,12 +70,14 @@ final class InvoiceModel
|
||||
ActivityStatisticService $activityStatistic,
|
||||
private readonly Customer $customer,
|
||||
private readonly InvoiceTemplate $template,
|
||||
private readonly RateCalculatorMode $rateCalculatorMode
|
||||
)
|
||||
{
|
||||
$this->invoiceDate = new \DateTimeImmutable();
|
||||
$this->formatter = $formatter;
|
||||
$this->addModelHydrator(new InvoiceModelDefaultHydrator());
|
||||
$this->addModelHydrator(new InvoiceModelCustomerHydrator($customerStatistic));
|
||||
$this->addModelHydrator(new InvoiceModelIssuerHydrator());
|
||||
$this->addModelHydrator(new InvoiceModelProjectHydrator($projectStatistic));
|
||||
$this->addModelHydrator(new InvoiceModelActivityHydrator($activityStatistic));
|
||||
$this->addModelHydrator(new InvoiceModelUserHydrator());
|
||||
@@ -207,6 +211,11 @@ final class InvoiceModel
|
||||
return $this->calculator;
|
||||
}
|
||||
|
||||
public function getRateCalculatorMode(): RateCalculatorMode
|
||||
{
|
||||
return $this->rateCalculatorMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user currently creating the invoice.
|
||||
*/
|
||||
|
||||
@@ -15,19 +15,21 @@ use App\Entity\Customer;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||
|
||||
final class InvoiceModelFactory
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CustomerStatisticService $customerStatisticService,
|
||||
private readonly ProjectStatisticService $projectStatisticService,
|
||||
private readonly ActivityStatisticService $activityStatisticService
|
||||
private readonly ActivityStatisticService $activityStatisticService,
|
||||
private readonly RateCalculatorMode $rateCalculatorMode
|
||||
) {
|
||||
}
|
||||
|
||||
public function createModel(InvoiceFormatter $formatter, Customer $customer, InvoiceTemplate $template, InvoiceQuery $query): InvoiceModel
|
||||
{
|
||||
$model = new InvoiceModel($formatter, $this->customerStatisticService, $this->projectStatisticService, $this->activityStatisticService, $customer, $template);
|
||||
$model = new InvoiceModel($formatter, $this->customerStatisticService, $this->projectStatisticService, $this->activityStatisticService, $customer, $template, $this->rateCalculatorMode);
|
||||
|
||||
$model->setQuery($query);
|
||||
|
||||
|
||||
@@ -49,11 +49,11 @@ final class ServiceInvoice
|
||||
private array $invoiceItemRepositories = [];
|
||||
|
||||
public function __construct(
|
||||
private InvoiceDocumentRepository $documents,
|
||||
private FileHelper $fileHelper,
|
||||
private InvoiceRepository $invoiceRepository,
|
||||
private LocaleService $formatter,
|
||||
private InvoiceModelFactory $invoiceModelFactory
|
||||
private readonly InvoiceDocumentRepository $documents,
|
||||
private readonly FileHelper $fileHelper,
|
||||
private readonly InvoiceRepository $invoiceRepository,
|
||||
private readonly LocaleService $formatter,
|
||||
private readonly InvoiceModelFactory $invoiceModelFactory
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use App\Timesheet\RateServiceInterface;
|
||||
*/
|
||||
final class RateCalculator implements CalculatorInterface
|
||||
{
|
||||
public function __construct(private RateServiceInterface $service)
|
||||
public function __construct(private readonly RateServiceInterface $service)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
31
src/Timesheet/RateCalculator/ClassicRateCalculator.php
Normal file
31
src/Timesheet/RateCalculator/ClassicRateCalculator.php
Normal 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\Timesheet\RateCalculator;
|
||||
|
||||
final class ClassicRateCalculator implements RateCalculatorMode
|
||||
{
|
||||
/**
|
||||
* Calculates the rate by an hourly rate and a given duration in seconds.
|
||||
*/
|
||||
public function calculateRate(float $hourlyRate, int $seconds): float
|
||||
{
|
||||
$rate = $hourlyRate * ($seconds / 3600);
|
||||
|
||||
return round($rate, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not round the duration, we keep the original sum.
|
||||
*/
|
||||
public function roundDuration(int $seconds): int
|
||||
{
|
||||
return $seconds;
|
||||
}
|
||||
}
|
||||
33
src/Timesheet/RateCalculator/DecimalRateCalculator.php
Normal file
33
src/Timesheet/RateCalculator/DecimalRateCalculator.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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\Timesheet\RateCalculator;
|
||||
|
||||
final class DecimalRateCalculator implements RateCalculatorMode
|
||||
{
|
||||
/**
|
||||
* Calculates the rate by an hourly rate and a given duration in seconds.
|
||||
*/
|
||||
public function calculateRate(float $hourlyRate, int $seconds): float
|
||||
{
|
||||
$rate = $hourlyRate * round(($seconds / 3600), 2, PHP_ROUND_HALF_UP);
|
||||
|
||||
return round($rate, 2, PHP_ROUND_HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure tha the duration is full compatible with decimal format, stripping away overflowing seconds.
|
||||
*/
|
||||
public function roundDuration(int $seconds): int
|
||||
{
|
||||
$decimal = round(($seconds / 3600), 2, PHP_ROUND_HALF_UP);
|
||||
|
||||
return (int) round(($decimal * 3600), 0, PHP_ROUND_HALF_UP);
|
||||
}
|
||||
}
|
||||
34
src/Timesheet/RateCalculator/RateCalculatorFactory.php
Normal file
34
src/Timesheet/RateCalculator/RateCalculatorFactory.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Timesheet\RateCalculator;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class RateCalculatorFactory
|
||||
{
|
||||
public function __construct(private readonly SystemConfiguration $configuration)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal do not call, use RateCalculatorMode injection
|
||||
*/
|
||||
public function getRateCalculatorMode(): RateCalculatorMode
|
||||
{
|
||||
if ($this->configuration->find('invoice.rounding_mode') === 'decimal') {
|
||||
return new DecimalRateCalculator();
|
||||
}
|
||||
|
||||
return new ClassicRateCalculator();
|
||||
}
|
||||
}
|
||||
23
src/Timesheet/RateCalculator/RateCalculatorMode.php
Normal file
23
src/Timesheet/RateCalculator/RateCalculatorMode.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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\Timesheet\RateCalculator;
|
||||
|
||||
interface RateCalculatorMode
|
||||
{
|
||||
/**
|
||||
* Calculates the rate by an hourly rate and a given duration in seconds.
|
||||
*/
|
||||
public function calculateRate(float $hourlyRate, int $seconds): float;
|
||||
|
||||
/**
|
||||
* * Makes sure tha the duration is fully rounded.
|
||||
*/
|
||||
public function roundDuration(int $seconds): int;
|
||||
}
|
||||
@@ -13,13 +13,18 @@ use App\Entity\RateInterface;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||
|
||||
/**
|
||||
* Implementation to calculate the rate for a timesheet record.
|
||||
*/
|
||||
final class RateService implements RateServiceInterface
|
||||
{
|
||||
public function __construct(private array $rates, private TimesheetRepository $repository)
|
||||
public function __construct(
|
||||
private readonly array $rates,
|
||||
private readonly TimesheetRepository $repository,
|
||||
private readonly RateCalculatorMode $calculatorMode
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -79,8 +84,8 @@ final class RateService implements RateServiceInterface
|
||||
$totalInternalRate = 0;
|
||||
|
||||
if (null !== $record->getDuration()) {
|
||||
$totalRate = Util::calculateRate($factoredHourlyRate, $record->getDuration());
|
||||
$totalInternalRate = Util::calculateRate($factoredInternalRate, $record->getDuration());
|
||||
$totalRate = $this->calculatorMode->calculateRate($factoredHourlyRate, $record->getDuration());
|
||||
$totalInternalRate = $this->calculatorMode->calculateRate($factoredInternalRate, $record->getDuration());
|
||||
}
|
||||
|
||||
return new Rate($totalRate, $totalInternalRate, $factoredHourlyRate, null);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Timesheet;
|
||||
|
||||
/**
|
||||
* A static helper class for re-usable functionality.
|
||||
* @deprecated use RateCalculatorMode instead
|
||||
*/
|
||||
final class Util
|
||||
{
|
||||
@@ -29,7 +29,7 @@ final class Util
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure tha the duration is full compatible with decimal format, stripping away overflowing seconds.
|
||||
* Makes sure that the duration is full compatible with decimal format, stripping away overflowing seconds.
|
||||
*/
|
||||
public static function decimalizeDuration(int $seconds): int
|
||||
{
|
||||
|
||||
@@ -15,9 +15,6 @@ use App\Entity\ProjectRate;
|
||||
use App\Entity\RateInterface;
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
*/
|
||||
trait RateControllerTestTrait
|
||||
{
|
||||
abstract protected function getRateUrl(?int $id = 1, ?int $rateId = null): string;
|
||||
|
||||
@@ -426,8 +426,8 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
|
||||
'exported' => true,
|
||||
'metaFields' => [],
|
||||
'hourlyRate' => 137.21,
|
||||
'rate' => 1772.75, // 12,92 * 137,21
|
||||
'internalRate' => 1772.75,
|
||||
'rate' => 1772.2958,
|
||||
'internalRate' => 1772.2958,
|
||||
];
|
||||
|
||||
foreach ($expected as $key => $value) {
|
||||
|
||||
@@ -16,7 +16,6 @@ use App\Form\Type\TagsType;
|
||||
use App\Tests\DataFixtures\TagFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Timesheet\Util;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
#[Group('integration')]
|
||||
@@ -414,7 +413,7 @@ class TimesheetTeamControllerTest extends AbstractControllerBaseTestCase
|
||||
self::assertCount(3, $timesheet->getTags());
|
||||
self::assertEquals($newUser->getId(), $timesheet->getUser()->getId());
|
||||
self::assertTrue($timesheet->isExported());
|
||||
self::assertEquals(Util::calculateRate(13.78, $timesheet->getDuration()), $timesheet->getRate());
|
||||
self::assertGreaterThan(0, $timesheet->getRate());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\Util;
|
||||
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
@@ -205,6 +206,7 @@ final class TimesheetFixtures implements TestFixture
|
||||
}
|
||||
}
|
||||
$manager->flush();
|
||||
$calculator = new ClassicRateCalculator();
|
||||
|
||||
for ($i = 0; $i < $this->amount; $i++) {
|
||||
$description = $faker->text();
|
||||
@@ -217,6 +219,9 @@ final class TimesheetFixtures implements TestFixture
|
||||
}
|
||||
|
||||
$user = $users[array_rand($users)];
|
||||
if ($user === null) {
|
||||
continue;
|
||||
}
|
||||
$activity = $activities[array_rand($activities)];
|
||||
$project = $activity->getProject();
|
||||
|
||||
@@ -225,6 +230,7 @@ final class TimesheetFixtures implements TestFixture
|
||||
}
|
||||
|
||||
$timesheet = $this->createTimesheetEntry(
|
||||
$calculator,
|
||||
$user,
|
||||
$activity,
|
||||
$project,
|
||||
@@ -244,12 +250,16 @@ final class TimesheetFixtures implements TestFixture
|
||||
$activity = $activities[array_rand($activities)];
|
||||
$project = $activity->getProject();
|
||||
$user = $users[array_rand($users)];
|
||||
if ($user === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === $project) {
|
||||
if ($project === null) {
|
||||
$project = $projects[array_rand($projects)];
|
||||
}
|
||||
|
||||
$timesheet = $this->createTimesheetEntry(
|
||||
$calculator,
|
||||
$user,
|
||||
$activity,
|
||||
$project,
|
||||
@@ -310,17 +320,25 @@ final class TimesheetFixtures implements TestFixture
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $start
|
||||
* @param array<Tag> $tagArray
|
||||
*/
|
||||
private function createTimesheetEntry(User $user, Activity $activity, Project $project, ?string $description, \DateTime $start, array $tagArray = [], bool $setEndDate = true): Timesheet
|
||||
private function createTimesheetEntry(
|
||||
RateCalculatorMode $calculatorMode,
|
||||
User $user,
|
||||
Activity $activity,
|
||||
Project $project,
|
||||
?string $description,
|
||||
\DateTime $start,
|
||||
array $tagArray = [],
|
||||
bool $setEndDate = true
|
||||
): Timesheet
|
||||
{
|
||||
$end = clone $start;
|
||||
$end = $end->modify('+ ' . (rand(1, 86400)) . ' seconds');
|
||||
|
||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||
$hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
$rate = Util::calculateRate($hourlyRate, $duration);
|
||||
$rate = $calculatorMode->calculateRate($hourlyRate, $duration);
|
||||
|
||||
$entry = new Timesheet();
|
||||
$entry->setActivity($activity);
|
||||
|
||||
@@ -310,6 +310,7 @@ class ConfigurationTest extends TestCase
|
||||
],
|
||||
'number_format' => '{Y}/{cy,3}',
|
||||
'upload_twig' => false,
|
||||
'rounding_mode' => 'classic',
|
||||
],
|
||||
'export' => [
|
||||
'documents' => [
|
||||
|
||||
@@ -48,9 +48,7 @@ class HtmlRendererTest extends AbstractRendererTestCase
|
||||
self::assertFalse($sut->isInternal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
#[Group('legacy')]
|
||||
public function testLegacy(): void
|
||||
{
|
||||
$sut = $this->getAbstractRenderer();
|
||||
|
||||
@@ -58,9 +58,7 @@ class PdfRendererTest extends AbstractRendererTestCase
|
||||
self::assertFalse($sut->isInternal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
#[Group('legacy')]
|
||||
public function testLegacy(): void
|
||||
{
|
||||
$sut = $this->getAbstractRenderer();
|
||||
|
||||
96
tests/Invoice/Hydrator/InvoiceModelIssuerHydratorTest.php
Normal file
96
tests/Invoice/Hydrator/InvoiceModelIssuerHydratorTest.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?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\Tests\Invoice\Hydrator;
|
||||
|
||||
use App\Invoice\Hydrator\InvoiceModelIssuerHydrator;
|
||||
use App\Tests\Invoice\Renderer\RendererTestTrait;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(InvoiceModelIssuerHydrator::class)]
|
||||
class InvoiceModelIssuerHydratorTest extends TestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
public function testHydrate(): void
|
||||
{
|
||||
$model = $this->getInvoiceModel();
|
||||
|
||||
$sut = new InvoiceModelIssuerHydrator();
|
||||
|
||||
$result = $sut->hydrate($model);
|
||||
$this->assertModelStructure($result);
|
||||
|
||||
$result = $sut->hydrate($model);
|
||||
|
||||
$this->assertModelStructure($result);
|
||||
|
||||
self::assertEquals([
|
||||
'issuer.id' => null,
|
||||
'issuer.address' => "Foo\nStreet\n1111 City",
|
||||
'issuer.address_line1' => '',
|
||||
'issuer.address_line2' => '',
|
||||
'issuer.address_line3' => '',
|
||||
'issuer.buyer_reference' => '',
|
||||
'issuer.city' => '',
|
||||
'issuer.postcode' => '',
|
||||
'issuer.name' => 'customer,with/special#name',
|
||||
'issuer.contact' => '',
|
||||
'issuer.company' => '',
|
||||
'issuer.vat_id' => '',
|
||||
'issuer.number' => '',
|
||||
'issuer.country' => 'AT',
|
||||
'issuer.country_name' => 'Austria',
|
||||
'issuer.homepage' => '',
|
||||
'issuer.comment' => '',
|
||||
'issuer.email' => '',
|
||||
'issuer.fax' => '',
|
||||
'issuer.phone' => '',
|
||||
'issuer.mobile' => '',
|
||||
'issuer.invoice_text' => '',
|
||||
'issuer.meta.foo-customer' => 'bar-customer',
|
||||
], $result);
|
||||
}
|
||||
|
||||
protected function assertModelStructure(array $model): void
|
||||
{
|
||||
$keys = [
|
||||
'issuer.id',
|
||||
'issuer.address',
|
||||
'issuer.address_line1',
|
||||
'issuer.address_line2',
|
||||
'issuer.address_line3',
|
||||
'issuer.buyer_reference',
|
||||
'issuer.city',
|
||||
'issuer.postcode',
|
||||
'issuer.name',
|
||||
'issuer.contact',
|
||||
'issuer.company',
|
||||
'issuer.vat_id',
|
||||
'issuer.country',
|
||||
'issuer.country_name',
|
||||
'issuer.number',
|
||||
'issuer.homepage',
|
||||
'issuer.comment',
|
||||
'issuer.email',
|
||||
'issuer.fax',
|
||||
'issuer.phone',
|
||||
'issuer.mobile',
|
||||
'issuer.meta.foo-customer',
|
||||
'issuer.invoice_text',
|
||||
];
|
||||
|
||||
$givenKeys = array_keys($model);
|
||||
sort($keys);
|
||||
sort($givenKeys);
|
||||
|
||||
self::assertEquals($keys, $givenKeys);
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,29 @@ class DebugRendererTest extends TestCase
|
||||
'invoice.subtotal',
|
||||
'invoice.subtotal_nc',
|
||||
'invoice.subtotal_plain',
|
||||
'issuer.address',
|
||||
'issuer.address_line1',
|
||||
'issuer.address_line2',
|
||||
'issuer.address_line3',
|
||||
'issuer.buyer_reference',
|
||||
'issuer.city',
|
||||
'issuer.comment',
|
||||
'issuer.company',
|
||||
'issuer.contact',
|
||||
'issuer.country',
|
||||
'issuer.country_name',
|
||||
'issuer.email',
|
||||
'issuer.fax',
|
||||
'issuer.homepage',
|
||||
'issuer.id',
|
||||
'issuer.invoice_text',
|
||||
'issuer.meta.foo-customer',
|
||||
'issuer.mobile',
|
||||
'issuer.name',
|
||||
'issuer.number',
|
||||
'issuer.phone',
|
||||
'issuer.postcode',
|
||||
'issuer.vat_id',
|
||||
'template.name',
|
||||
'template.company',
|
||||
'template.country',
|
||||
|
||||
@@ -54,9 +54,17 @@ trait RendererTestTrait
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $classname
|
||||
*/
|
||||
protected function getAbstractRenderer(string $classname): AbstractRenderer
|
||||
{
|
||||
return new $classname();
|
||||
$t = new $classname();
|
||||
if (!$t instanceof AbstractRenderer) {
|
||||
throw new \InvalidArgumentException('Not an instance of AbstractRenderer: ' . \get_class($t));
|
||||
}
|
||||
|
||||
return $t;
|
||||
}
|
||||
|
||||
protected function getFormatter(): InvoiceFormatter
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
namespace App\Tests\Mocks;
|
||||
|
||||
use PHPUnit\Framework\MockObject\MockBuilder;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
abstract class AbstractMockFactory
|
||||
{
|
||||
public function __construct(private TestCase $testCase)
|
||||
public function __construct(private readonly TestCase $testCase)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -23,11 +24,19 @@ abstract class AbstractMockFactory
|
||||
return $this->testCase;
|
||||
}
|
||||
|
||||
protected function createMock(string $className)
|
||||
/**
|
||||
* @template T of object
|
||||
* @param class-string<T> $className
|
||||
* @return T&MockObject
|
||||
*/
|
||||
protected function createMock(string $className): object
|
||||
{
|
||||
return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock();
|
||||
return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock(); // @phpstan-ignore return.type
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $className
|
||||
*/
|
||||
protected function getMockBuilder(string $className): MockBuilder
|
||||
{
|
||||
return new MockBuilder($this->testCase, $className);
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Activity\ActivityStatisticService;
|
||||
use App\Customer\CustomerStatisticService;
|
||||
use App\Invoice\InvoiceModelFactory;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Timesheet\RateCalculator\DecimalRateCalculator;
|
||||
|
||||
class InvoiceModelFactoryFactory extends AbstractMockFactory
|
||||
{
|
||||
@@ -24,7 +25,8 @@ class InvoiceModelFactoryFactory extends AbstractMockFactory
|
||||
$projectStatistic = $this->getMockBuilder(ProjectStatisticService::class)->disableOriginalConstructor()->getMock();
|
||||
/** @var ActivityStatisticService $activityStatistic */
|
||||
$activityStatistic = $this->getMockBuilder(ActivityStatisticService::class)->disableOriginalConstructor()->getMock();
|
||||
$rateMode = new DecimalRateCalculator();
|
||||
|
||||
return new InvoiceModelFactory($customerStatistic, $projectStatistic, $activityStatistic);
|
||||
return new InvoiceModelFactory($customerStatistic, $projectStatistic, $activityStatistic, $rateMode);
|
||||
}
|
||||
}
|
||||
|
||||
27
tests/Mocks/RateServiceFactory.php
Normal file
27
tests/Mocks/RateServiceFactory.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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\Tests\Mocks;
|
||||
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||
use App\Timesheet\RateService;
|
||||
|
||||
class RateServiceFactory extends AbstractMockFactory
|
||||
{
|
||||
public function create(array $rules = [], array $rates = []): RateService
|
||||
{
|
||||
$mock = $this->createMock(TimesheetRepository::class);
|
||||
if (!empty($rates)) {
|
||||
$mock->expects($this->getTestCase()->any())->method('findMatchingRates')->willReturn($rates);
|
||||
}
|
||||
|
||||
return new RateService($rules, $mock, new ClassicRateCalculator());
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ class SystemConfigurationFactory
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $settings
|
||||
* @return SystemConfiguration
|
||||
*/
|
||||
public static function create(ConfigLoaderInterface $repository, array $settings): SystemConfiguration
|
||||
{
|
||||
@@ -26,7 +25,6 @@ class SystemConfigurationFactory
|
||||
|
||||
/**
|
||||
* @param array<mixed> $settings
|
||||
* @return SystemConfiguration
|
||||
*/
|
||||
public static function createStub(array $settings = []): SystemConfiguration
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ use App\Entity\ProjectRate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Tests\Mocks\RateServiceFactory;
|
||||
use App\Timesheet\Calculator\RateCalculator;
|
||||
use App\Timesheet\RateService;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
@@ -28,14 +28,11 @@ use PHPUnit\Framework\TestCase;
|
||||
#[CoversClass(RateCalculator::class)]
|
||||
class RateCalculatorTest extends TestCase
|
||||
{
|
||||
protected function getRateRepositoryMock(array $rates = []): TimesheetRepository
|
||||
private function getRateService(array $rules = [], array $rates = []): RateService
|
||||
{
|
||||
$mock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
if (!empty($rates)) {
|
||||
$mock->expects($this->any())->method('findMatchingRates')->willReturn($rates);
|
||||
}
|
||||
$factory = new RateServiceFactory($this);
|
||||
|
||||
return $mock;
|
||||
return $factory->create($rules, $rates);
|
||||
}
|
||||
|
||||
private function assertRateByTimesheetHourlyRate(int $duration, float $hourlyRate, float $rate): void
|
||||
@@ -47,7 +44,7 @@ class RateCalculatorTest extends TestCase
|
||||
$record->setActivity(new Activity());
|
||||
$record->setUser($this->getTestUser());
|
||||
|
||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
||||
$sut = new RateCalculator($this->getRateService());
|
||||
$sut->calculate($record, []);
|
||||
self::assertEquals($rate, $record->getRate());
|
||||
}
|
||||
@@ -55,9 +52,9 @@ class RateCalculatorTest extends TestCase
|
||||
public function testCalculateWithTimesheetHourlyRate(): void
|
||||
{
|
||||
$this->assertRateByTimesheetHourlyRate(1800, 100, 50);
|
||||
$this->assertRateByTimesheetHourlyRate(400, 100, 11);
|
||||
$this->assertRateByTimesheetHourlyRate(1234, 100, 34);
|
||||
$this->assertRateByTimesheetHourlyRate(2739, 100, 76);
|
||||
$this->assertRateByTimesheetHourlyRate(400, 100, 11.1111);
|
||||
$this->assertRateByTimesheetHourlyRate(1234, 100, 34.2778);
|
||||
$this->assertRateByTimesheetHourlyRate(2739, 100, 76.0833);
|
||||
}
|
||||
|
||||
public function testCalculateWithTimesheetFixedRate(): void
|
||||
@@ -71,7 +68,7 @@ class RateCalculatorTest extends TestCase
|
||||
$record->setActivity(new Activity());
|
||||
$record->setUser($this->getTestUser());
|
||||
|
||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
||||
$sut = new RateCalculator($this->getRateService());
|
||||
$sut->calculate($record, []);
|
||||
self::assertEquals(10, $record->getRate());
|
||||
}
|
||||
@@ -112,22 +109,22 @@ class RateCalculatorTest extends TestCase
|
||||
|
||||
#[DataProvider('getRateTestData')]
|
||||
public function testRates(
|
||||
$expectedRate,
|
||||
$expectedInternalRate,
|
||||
$duration,
|
||||
$userRate,
|
||||
$userInternalRate,
|
||||
$timesheetHourly,
|
||||
$timesheetFixed,
|
||||
$activityRate,
|
||||
$activityInternal,
|
||||
$activityIsFixed,
|
||||
$projectRate,
|
||||
$projectInternal,
|
||||
$projectIsFixed,
|
||||
$customerRate,
|
||||
$customerInternal,
|
||||
$customerIsFixed
|
||||
float $expectedRate,
|
||||
float $expectedInternalRate,
|
||||
int $duration,
|
||||
float $userRate,
|
||||
?float $userInternalRate,
|
||||
?float $timesheetHourly,
|
||||
?float $timesheetFixed,
|
||||
?float $activityRate,
|
||||
?float $activityInternal,
|
||||
bool $activityIsFixed,
|
||||
?float $projectRate,
|
||||
?float $projectInternal,
|
||||
bool $projectIsFixed,
|
||||
?float $customerRate,
|
||||
?float $customerInternal,
|
||||
bool $customerIsFixed
|
||||
) {
|
||||
$customer = new Customer('foo');
|
||||
|
||||
@@ -178,7 +175,7 @@ class RateCalculatorTest extends TestCase
|
||||
$rates[] = $rate;
|
||||
}
|
||||
|
||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock($rates)));
|
||||
$sut = new RateCalculator($this->getRateService([], $rates));
|
||||
$sut->calculate($timesheet, []);
|
||||
self::assertEquals($expectedRate, $timesheet->getRate());
|
||||
self::assertEquals($expectedInternalRate, $timesheet->getInternalRate());
|
||||
@@ -207,7 +204,7 @@ class RateCalculatorTest extends TestCase
|
||||
|
||||
self::assertEquals(0, $record->getRate());
|
||||
|
||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
||||
$sut = new RateCalculator($this->getRateService());
|
||||
$sut->calculate($record, []);
|
||||
self::assertEquals(0, $record->getRate());
|
||||
}
|
||||
@@ -216,7 +213,7 @@ class RateCalculatorTest extends TestCase
|
||||
* Uses the hourly rate from user_preferences to calculate the rate.
|
||||
*/
|
||||
#[DataProvider('getRuleDefinitions')]
|
||||
public function testCalculateWithRulesByUsersHourlyRate($duration, $rules, $expectedRate): void
|
||||
public function testCalculateWithRulesByUsersHourlyRate(int $duration, array $rules, float $expectedRate): void
|
||||
{
|
||||
$end = new \DateTime('12:00:00', new \DateTimeZone('UTC'));
|
||||
$start = clone $end;
|
||||
@@ -232,7 +229,7 @@ class RateCalculatorTest extends TestCase
|
||||
|
||||
$record->setEnd($end);
|
||||
|
||||
$sut = new RateCalculator(new RateService($rules, $this->getRateRepositoryMock()));
|
||||
$sut = new RateCalculator($this->getRateService($rules));
|
||||
$sut->calculate($record, []);
|
||||
|
||||
self::assertEquals($expectedRate, $record->getRate());
|
||||
@@ -247,7 +244,7 @@ class RateCalculatorTest extends TestCase
|
||||
[
|
||||
31837, // 31824 = 8,84
|
||||
[],
|
||||
663
|
||||
663.2708
|
||||
],
|
||||
[
|
||||
31837, // 31824 = 8,84
|
||||
@@ -261,7 +258,7 @@ class RateCalculatorTest extends TestCase
|
||||
'factor' => 1.5
|
||||
],
|
||||
],
|
||||
1326 // 8,84 * 75 (see user) * 2
|
||||
1326.5417
|
||||
],
|
||||
[
|
||||
31837, // 31824 = 8,84
|
||||
@@ -275,7 +272,7 @@ class RateCalculatorTest extends TestCase
|
||||
'factor' => 1.5
|
||||
],
|
||||
],
|
||||
2320.5 // 75 * 8,84 * 3,5
|
||||
2321.4479
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
130
tests/Timesheet/RateCalculator/ClassicRateCalculatorTest.php
Normal file
130
tests/Timesheet/RateCalculator/ClassicRateCalculatorTest.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?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\Tests\Timesheet\RateCalculator;
|
||||
|
||||
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(ClassicRateCalculator::class)]
|
||||
class ClassicRateCalculatorTest extends TestCase
|
||||
{
|
||||
#[DataProvider('provideRates')]
|
||||
public function testCalculateRate(float $hourlyRate, int $seconds, float $expected): void
|
||||
{
|
||||
$sut = new ClassicRateCalculator();
|
||||
|
||||
$result = $sut->calculateRate($hourlyRate, $seconds);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function provideRates(): array
|
||||
{
|
||||
return [
|
||||
'zero duration' => [100.0, 0, 0.0],
|
||||
'full hour' => [100.0, 3600, 100.0],
|
||||
'half hour' => [100.0, 1800, 50.0],
|
||||
'one minute with rounding' => [123.4567, 60, 2.0576],
|
||||
'one second tiny amount' => [1.23, 1, 0.0003],
|
||||
];
|
||||
}
|
||||
|
||||
public function testRoundDurationKeepsOriginalSeconds(): void
|
||||
{
|
||||
$sut = new ClassicRateCalculator();
|
||||
|
||||
$this->assertSame(0, $sut->roundDuration(0));
|
||||
$this->assertSame(59, $sut->roundDuration(59));
|
||||
$this->assertSame(3601, $sut->roundDuration(3601));
|
||||
}
|
||||
|
||||
#[DataProvider('getRateCalculationData')]
|
||||
public function testCalculateRates(float $hourlyRate, int $duration, float $expectedRate): void
|
||||
{
|
||||
$sut = new ClassicRateCalculator();
|
||||
self::assertEquals($expectedRate, $sut->calculateRate($hourlyRate, $duration));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<float, int, >>|\Generator
|
||||
*/
|
||||
public static function getRateCalculationData()
|
||||
{
|
||||
yield [0, 0, 0];
|
||||
yield [1, 100, 0.0278];
|
||||
yield [1, 900, 0.25];
|
||||
yield [1, 1800, 0.5];
|
||||
yield [10000, 1, 2.7778];
|
||||
yield [736, 123, 25.1467];
|
||||
yield [7360, 1234, 2522.8444];
|
||||
yield [7360.34, 1234, 2522.961];
|
||||
yield [7360.01, 1234, 2522.8479];
|
||||
yield [7360.99, 1234, 2523.1838];
|
||||
}
|
||||
|
||||
public function testCalculateRateWithRounding(): void
|
||||
{
|
||||
$total = 0.00;
|
||||
$seconds = 0;
|
||||
$repeat = 130;
|
||||
$sut = new ClassicRateCalculator();
|
||||
|
||||
for ($a = 0; $a < $repeat; $a++) {
|
||||
$inputs = [
|
||||
900,
|
||||
1600,
|
||||
4200,
|
||||
8763,
|
||||
3300,
|
||||
600,
|
||||
1300,
|
||||
1837,
|
||||
4217,
|
||||
5400,
|
||||
3283,
|
||||
600,
|
||||
];
|
||||
|
||||
foreach ($inputs as $i) {
|
||||
$seconds += $i;
|
||||
$total += $sut->calculateRate(114.75, $i);
|
||||
}
|
||||
}
|
||||
|
||||
self::assertEquals(36000 * $repeat, $seconds);
|
||||
self::assertEquals(1147.50 * $repeat, $total);
|
||||
}
|
||||
|
||||
public function testDecimalDuration(): void
|
||||
{
|
||||
$inputs = [
|
||||
900,
|
||||
1600,
|
||||
4200,
|
||||
8763,
|
||||
3300,
|
||||
600,
|
||||
1300,
|
||||
1837,
|
||||
4217,
|
||||
5400,
|
||||
3283,
|
||||
600,
|
||||
7200,
|
||||
];
|
||||
|
||||
$sut = new ClassicRateCalculator();
|
||||
foreach ($inputs as $row) {
|
||||
self::assertEquals($row, $sut->roundDuration($row));
|
||||
}
|
||||
}
|
||||
}
|
||||
142
tests/Timesheet/RateCalculator/DecimalRateCalculatorTest.php
Normal file
142
tests/Timesheet/RateCalculator/DecimalRateCalculatorTest.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?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\Tests\Timesheet\RateCalculator;
|
||||
|
||||
use App\Timesheet\RateCalculator\DecimalRateCalculator;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(DecimalRateCalculator::class)]
|
||||
class DecimalRateCalculatorTest extends TestCase
|
||||
{
|
||||
#[DataProvider('provideRates')]
|
||||
public function testCalculateRate(float $hourlyRate, int $seconds, float $expected): void
|
||||
{
|
||||
$sut = new DecimalRateCalculator();
|
||||
|
||||
$result = $sut->calculateRate($hourlyRate, $seconds);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function provideRates(): array
|
||||
{
|
||||
return [
|
||||
'zero duration' => [100.0, 0, 0.0],
|
||||
'full hour' => [100.0, 3600, 100.0],
|
||||
'half hour' => [100.0, 1800, 50.0],
|
||||
'one minute with rounding' => [123.4567, 60, 2.47],
|
||||
'one second tiny amount' => [1.23, 1, 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function testRoundDurationKeepsOriginalSeconds(): void
|
||||
{
|
||||
$sut = new DecimalRateCalculator();
|
||||
|
||||
$this->assertSame(0, $sut->roundDuration(0));
|
||||
$this->assertSame(36, $sut->roundDuration(45));
|
||||
$this->assertSame(72, $sut->roundDuration(59));
|
||||
$this->assertSame(1224, $sut->roundDuration(1234));
|
||||
$this->assertSame(3600, $sut->roundDuration(3601));
|
||||
}
|
||||
|
||||
#[DataProvider('getRateCalculationData')]
|
||||
public function testCalculateRates(float $hourlyRate, int $duration, float $expectedRate): void
|
||||
{
|
||||
$sut = new DecimalRateCalculator();
|
||||
self::assertEquals($expectedRate, $sut->calculateRate($hourlyRate, $duration));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<float, int, >>|\Generator
|
||||
*/
|
||||
public static function getRateCalculationData()
|
||||
{
|
||||
yield [0.00, 0, 0.00];
|
||||
yield [10.00, 7260, 20.2];
|
||||
yield [1.00, 3600, 1.00];
|
||||
yield [1.00, 100, 0.03];
|
||||
yield [1.00, 900, 0.25];
|
||||
yield [1.00, 1800, 0.5];
|
||||
yield [10000.00, 60, 200.00];
|
||||
yield [736.00, 123, 22.08];
|
||||
yield [7360.00, 1234, 2502.4];
|
||||
yield [7360.34, 1234, 2502.52];
|
||||
yield [7360.01, 1234, 2502.4];
|
||||
yield [7360.99, 1234, 2502.74];
|
||||
}
|
||||
|
||||
public function testCalculateRateWithRounding(): void
|
||||
{
|
||||
$total = 0.00;
|
||||
$seconds = 0;
|
||||
$repeat = 130;
|
||||
|
||||
$inputs = [
|
||||
[900, 28.69, 0],
|
||||
[1600, 50.49, 0],
|
||||
[4200, 134.26, 0],
|
||||
[8763, 278.84, 0],
|
||||
[3300, 105.57, 0],
|
||||
[600, 19.51, 0],
|
||||
[1300, 41.31, 0],
|
||||
[1837, 58.52, 0],
|
||||
[4217, 134.26, 0],
|
||||
[5400, 172.13, 0],
|
||||
[3283, 104.42, 0],
|
||||
[600, 19.51, 0],
|
||||
];
|
||||
|
||||
$totalExpected = 0.00;
|
||||
$sut = new DecimalRateCalculator();
|
||||
|
||||
for ($a = 0; $a < $repeat; $a++) {
|
||||
foreach ($inputs as $row) {
|
||||
[$duration, $rate] = $row;
|
||||
$seconds += $duration;
|
||||
$totalExpected += $rate;
|
||||
$tmp = $sut->calculateRate(114.75, $duration);
|
||||
self::assertEquals($rate, $tmp);
|
||||
$total += $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
self::assertEquals(36000 * $repeat, $seconds);
|
||||
self::assertEquals($totalExpected, $total);
|
||||
self::assertEqualsWithDelta(1147.51 * $repeat, $total, 0.00001);
|
||||
self::assertEqualsWithDelta(149176.3, $total, 0.00001);
|
||||
}
|
||||
|
||||
public function testDecimalDuration(): void
|
||||
{
|
||||
$inputs = [
|
||||
[900, 900],
|
||||
[1600, 1584],
|
||||
[4200, 4212],
|
||||
[8763, 8748],
|
||||
[3300, 3312],
|
||||
[600, 612],
|
||||
[1300, 1296],
|
||||
[1837, 1836],
|
||||
[4217, 4212],
|
||||
[5400, 5400],
|
||||
[3283, 3276],
|
||||
[600, 612],
|
||||
[7200, 7200],
|
||||
];
|
||||
|
||||
$sut = new DecimalRateCalculator();
|
||||
foreach ($inputs as $row) {
|
||||
self::assertEquals($row[1], $sut->roundDuration($row[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
48
tests/Timesheet/RateCalculator/RateCalculatorFactoryTest.php
Normal file
48
tests/Timesheet/RateCalculator/RateCalculatorFactoryTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Tests\Timesheet\RateCalculator;
|
||||
|
||||
use App\Tests\Mocks\SystemConfigurationFactory;
|
||||
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||
use App\Timesheet\RateCalculator\DecimalRateCalculator;
|
||||
use App\Timesheet\RateCalculator\RateCalculatorFactory;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(RateCalculatorFactory::class)]
|
||||
class RateCalculatorFactoryTest extends TestCase
|
||||
{
|
||||
private function assertCreatesClassicConfig(array $config): void
|
||||
{
|
||||
$config = SystemConfigurationFactory::createStub($config);
|
||||
$sut = new RateCalculatorFactory($config);
|
||||
|
||||
$mode = $sut->getRateCalculatorMode();
|
||||
self::assertInstanceOf(ClassicRateCalculator::class, $mode);
|
||||
}
|
||||
|
||||
public function testCreatesClassic(): void
|
||||
{
|
||||
$this->assertCreatesClassicConfig([]);
|
||||
$this->assertCreatesClassicConfig(['invoice' => ['rounding_mode' => 'classic']]);
|
||||
$this->assertCreatesClassicConfig(['invoice' => ['rounding_mode' => '']]);
|
||||
$this->assertCreatesClassicConfig(['invoice' => ['rounding_mode' => 'foo']]);
|
||||
$this->assertCreatesClassicConfig(['invoice' => ['rounding_mode' => 'DECIMAL']]);
|
||||
}
|
||||
|
||||
public function testCreateWithDecimalConfig(): void
|
||||
{
|
||||
$config = SystemConfigurationFactory::createStub(['invoice' => ['rounding_mode' => 'decimal']]);
|
||||
$sut = new RateCalculatorFactory($config);
|
||||
|
||||
$mode = $sut->getRateCalculatorMode();
|
||||
self::assertInstanceOf(DecimalRateCalculator::class, $mode);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use App\Entity\ProjectRate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Tests\Mocks\RateServiceFactory;
|
||||
use App\Timesheet\RateService;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
@@ -27,14 +27,11 @@ use PHPUnit\Framework\TestCase;
|
||||
#[CoversClass(RateService::class)]
|
||||
class RateServiceTest extends TestCase
|
||||
{
|
||||
protected function getRateRepositoryMock(array $rates = []): TimesheetRepository
|
||||
private function getSut(array $rules = [], array $rates = []): RateService
|
||||
{
|
||||
$mock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
if (!empty($rates)) {
|
||||
$mock->expects($this->any())->method('findMatchingRates')->willReturn($rates);
|
||||
}
|
||||
$factory = new RateServiceFactory($this);
|
||||
|
||||
return $mock;
|
||||
return $factory->create($rules, $rates);
|
||||
}
|
||||
|
||||
private static function createDateTime(?string $datetime = null): \DateTime
|
||||
@@ -51,7 +48,7 @@ class RateServiceTest extends TestCase
|
||||
$record->setActivity(new Activity());
|
||||
$record->setUser($this->getTestUser());
|
||||
|
||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
||||
$sut = $this->getSut();
|
||||
$rate = $sut->calculate($record);
|
||||
self::assertEquals(50, $rate->getRate());
|
||||
}
|
||||
@@ -67,7 +64,7 @@ class RateServiceTest extends TestCase
|
||||
$record->setActivity(new Activity());
|
||||
$record->setUser($this->getTestUser());
|
||||
|
||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
||||
$sut = $this->getSut();
|
||||
$rate = $sut->calculate($record);
|
||||
self::assertEquals(10, $rate->getRate());
|
||||
}
|
||||
@@ -108,22 +105,22 @@ class RateServiceTest extends TestCase
|
||||
|
||||
#[DataProvider('getRateTestData')]
|
||||
public function testRates(
|
||||
$expectedRate,
|
||||
$expectedInternalRate,
|
||||
$duration,
|
||||
$userRate,
|
||||
$userInternalRate,
|
||||
$timesheetHourly,
|
||||
$timesheetFixed,
|
||||
$activityRate,
|
||||
$activityInternal,
|
||||
$activityIsFixed,
|
||||
$projectRate,
|
||||
$projectInternal,
|
||||
$projectIsFixed,
|
||||
$customerRate,
|
||||
$customerInternal,
|
||||
$customerIsFixed
|
||||
float $expectedRate,
|
||||
float $expectedInternalRate,
|
||||
int $duration,
|
||||
float $userRate,
|
||||
?float $userInternalRate,
|
||||
?float $timesheetHourly,
|
||||
?float $timesheetFixed,
|
||||
?float $activityRate,
|
||||
?float $activityInternal,
|
||||
bool $activityIsFixed,
|
||||
?float $projectRate,
|
||||
?float $projectInternal,
|
||||
bool $projectIsFixed,
|
||||
?float $customerRate,
|
||||
?float $customerInternal,
|
||||
bool $customerIsFixed
|
||||
): void {
|
||||
$customer = new Customer('foo');
|
||||
|
||||
@@ -174,7 +171,7 @@ class RateServiceTest extends TestCase
|
||||
$rates[] = $rate;
|
||||
}
|
||||
|
||||
$sut = new RateService([], $this->getRateRepositoryMock($rates));
|
||||
$sut = $this->getSut([], $rates);
|
||||
$rate = $sut->calculate($timesheet);
|
||||
self::assertEquals($expectedRate, $rate->getRate());
|
||||
self::assertEquals($expectedInternalRate, $rate->getInternalRate());
|
||||
@@ -203,7 +200,7 @@ class RateServiceTest extends TestCase
|
||||
|
||||
self::assertEquals(0, $record->getRate());
|
||||
|
||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
||||
$sut = $this->getSut();
|
||||
$rate = $sut->calculate($record);
|
||||
self::assertEquals(0, $rate->getRate());
|
||||
}
|
||||
@@ -212,7 +209,7 @@ class RateServiceTest extends TestCase
|
||||
* Uses the hourly rate from user_preferences to calculate the rate.
|
||||
*/
|
||||
#[DataProvider('getRuleDefinitions')]
|
||||
public function testCalculateWithRulesByUsersHourlyRate($duration, $rules, $expectedRate): void
|
||||
public function testCalculateWithRulesByUsersHourlyRate(int $duration, array $rules, float $expectedRate): void
|
||||
{
|
||||
$end = self::createDateTime('12:00:00');
|
||||
$start = clone $end;
|
||||
@@ -228,7 +225,7 @@ class RateServiceTest extends TestCase
|
||||
|
||||
$record->setEnd($end);
|
||||
|
||||
$sut = new RateService($rules, $this->getRateRepositoryMock());
|
||||
$sut = $this->getSut($rules);
|
||||
$rate = $sut->calculate($record);
|
||||
|
||||
self::assertEquals($expectedRate, $rate->getRate());
|
||||
@@ -243,7 +240,7 @@ class RateServiceTest extends TestCase
|
||||
[
|
||||
31837,
|
||||
[],
|
||||
663
|
||||
663.2708
|
||||
],
|
||||
[
|
||||
31837,
|
||||
@@ -257,7 +254,7 @@ class RateServiceTest extends TestCase
|
||||
'factor' => 1.5
|
||||
],
|
||||
],
|
||||
1326 // 8,84 * 75 (see user) * 2
|
||||
1326.5417
|
||||
],
|
||||
[
|
||||
31837,
|
||||
@@ -271,7 +268,7 @@ class RateServiceTest extends TestCase
|
||||
'factor' => 1.5
|
||||
],
|
||||
],
|
||||
2320.5 // 75 * 8,84 * 3,5
|
||||
2321.4479
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,11 +12,14 @@ namespace App\Tests\Timesheet;
|
||||
use App\Timesheet\Util;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[Group('legacy')]
|
||||
#[CoversClass(Util::class)]
|
||||
class UtilTest extends TestCase
|
||||
{
|
||||
#[Group('legacy')]
|
||||
#[DataProvider('getRateCalculationData')]
|
||||
public function testCalculateRate(float $hourlyRate, int $duration, float $expectedRate): void
|
||||
{
|
||||
@@ -42,6 +45,7 @@ class UtilTest extends TestCase
|
||||
yield [7360.99, 1234, 2502.74];
|
||||
}
|
||||
|
||||
#[Group('legacy')]
|
||||
public function testCalculateRateWithRounding(): void
|
||||
{
|
||||
$total = 0.00;
|
||||
@@ -82,6 +86,7 @@ class UtilTest extends TestCase
|
||||
self::assertEqualsWithDelta(149176.3, $total, 0.00001);
|
||||
}
|
||||
|
||||
#[Group('legacy')]
|
||||
public function testDecimalDuration(): void
|
||||
{
|
||||
$inputs = [
|
||||
|
||||
@@ -27,6 +27,7 @@ use App\Project\ProjectStatisticService;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Tests\Mocks\SystemConfigurationFactory;
|
||||
use App\Timesheet\Rate;
|
||||
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||
use App\Timesheet\RateService;
|
||||
use App\Timesheet\RateServiceInterface;
|
||||
use App\Validator\Constraints\TimesheetBudgetUsed;
|
||||
@@ -92,7 +93,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase
|
||||
$rateService = $this->createMock(RateServiceInterface::class);
|
||||
$rateService->method('calculate')->willReturn($rate);
|
||||
} else {
|
||||
$rateService = new RateService([], $timesheetRepository);
|
||||
$rateService = new RateService([], $timesheetRepository, new ClassicRateCalculator());
|
||||
}
|
||||
|
||||
$auth = $this->createMock(AuthorizationCheckerInterface::class);
|
||||
|
||||
@@ -966,11 +966,6 @@ parameters:
|
||||
count: 1
|
||||
path: Controller/TimesheetTeamControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$seconds of static method App\\\\Timesheet\\\\Util\\:\\:calculateRate\\(\\) expects int, int\\|null given\\.$#"
|
||||
count: 1
|
||||
path: Controller/TimesheetTeamControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
|
||||
count: 2
|
||||
@@ -1411,61 +1406,31 @@ parameters:
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceItemDefaultHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelActivityHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelActivityHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelActivityHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelActivityHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelCustomerHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelCustomerHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelDefaultHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelDefaultHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelProjectHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelProjectHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelProjectHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelProjectHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelUserHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelUserHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelUserHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Hydrator/InvoiceModelUserHydratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\DateNumberGeneratorTest\\:\\:getSut\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -1521,11 +1486,6 @@ parameters:
|
||||
count: 1
|
||||
path: Invoice/Renderer/DebugRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DebugRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Renderer/DebugRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DebugRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -1576,16 +1536,6 @@ parameters:
|
||||
count: 1
|
||||
path: Invoice/Renderer/DebugRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DocxRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Renderer/DocxRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\OdsRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Renderer/OdsRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\OdsRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -1621,11 +1571,6 @@ parameters:
|
||||
count: 1
|
||||
path: Invoice/Renderer/OdsRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\PdfRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Renderer/PdfRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -1636,11 +1581,6 @@ parameters:
|
||||
count: 3
|
||||
path: Invoice/Renderer/PdfRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\TwigRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Renderer/TwigRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
@@ -1651,11 +1591,6 @@ parameters:
|
||||
count: 3
|
||||
path: Invoice/Renderer/TwigRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\XlsxRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#"
|
||||
count: 1
|
||||
path: Invoice/Renderer/XlsxRendererTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\XlsxRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -1731,20 +1666,11 @@ parameters:
|
||||
count: 1
|
||||
path: Ldap/LdapManagerTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Mocks\\\\AbstractMockFactory\\:\\:createMock\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Mocks/AbstractMockFactory.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Mocks\\\\AbstractMockFactory\\:\\:getMockBuilder\\(\\) return type with generic class PHPUnit\\\\Framework\\\\MockObject\\\\MockBuilder does not specify its types\\: TMockedClass$#"
|
||||
count: 1
|
||||
path: Mocks/AbstractMockFactory.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$type of class PHPUnit\\\\Framework\\\\MockObject\\\\MockBuilder constructor expects class\\-string\\<TMockedClass\\>, string given\\.$#"
|
||||
count: 1
|
||||
path: Mocks/AbstractMockFactory.php
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$dataDir of class App\\\\Utils\\\\FileHelper constructor expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
@@ -1935,116 +1861,16 @@ parameters:
|
||||
count: 1
|
||||
path: Timesheet/Calculator/DurationCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getRateRepositoryMock\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getRateTestData\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$duration with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$expectedRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$rules with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$activityInternal with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$activityIsFixed with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$activityRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$customerInternal with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$customerIsFixed with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$customerRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$duration with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$expectedInternalRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$expectedRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$projectInternal with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$projectIsFixed with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$projectRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$timesheetFixed with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$timesheetHourly with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$userInternalRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$userRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\DateTimeFactoryTest\\:\\:getEndOfWeekData\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -2065,111 +1891,11 @@ parameters:
|
||||
count: 1
|
||||
path: Timesheet/LockdownServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getRateRepositoryMock\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getRateTestData\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$duration with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$expectedRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$rules with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$activityInternal with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$activityIsFixed with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$activityRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$customerInternal with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$customerIsFixed with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$customerRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$duration with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$expectedInternalRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$expectedRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$projectInternal with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$projectIsFixed with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$projectRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$timesheetFixed with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$timesheetHourly with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$userInternalRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$userRate with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Timesheet/RateServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#"
|
||||
count: 6
|
||||
@@ -2924,3 +2650,29 @@ parameters:
|
||||
message: "#^Method App\\\\Tests\\\\Widget\\\\Type\\\\UserDurationYearTest\\:\\:getDefaultOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Widget/Type/UserDurationYearTest.php
|
||||
-
|
||||
message: '''
|
||||
#^Access to constant on deprecated class App\\Timesheet\\Util\:
|
||||
use RateCalculatorMode instead$#
|
||||
'''
|
||||
identifier: classConstant.deprecatedClass
|
||||
count: 1
|
||||
path: Timesheet/UtilTest.php
|
||||
|
||||
-
|
||||
message: '''
|
||||
#^Call to method calculateRate\(\) of deprecated class App\\Timesheet\\Util\:
|
||||
use RateCalculatorMode instead$#
|
||||
'''
|
||||
identifier: staticMethod.deprecatedClass
|
||||
count: 2
|
||||
path: Timesheet/UtilTest.php
|
||||
|
||||
-
|
||||
message: '''
|
||||
#^Call to method decimalizeDuration\(\) of deprecated class App\\Timesheet\\Util\:
|
||||
use RateCalculatorMode instead$#
|
||||
'''
|
||||
identifier: staticMethod.deprecatedClass
|
||||
count: 1
|
||||
path: Timesheet/UtilTest.php
|
||||
|
||||
@@ -310,6 +310,10 @@
|
||||
<source>copy_teams_on_create</source>
|
||||
<target>Übernehme Teams beim Erstellen neuer Einträge vom angemeldeten Benutzer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6AaVxB2" resname="invoice.rounding_mode" approved="yes">
|
||||
<source>invoice.rounding_mode</source>
|
||||
<target>Rundungsmodus</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -310,6 +310,10 @@
|
||||
<source>copy_teams_on_create</source>
|
||||
<target>Take over teams from the logged-in user when creating new entries</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6AaVxB2" resname="invoice.rounding_mode" approved="yes">
|
||||
<source>invoice.rounding_mode</source>
|
||||
<target>Rounding mode</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Reference in New Issue
Block a user