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:
|
App\Timesheet\RateService:
|
||||||
arguments: ['%kimai.timesheet.rates%']
|
arguments: ['%kimai.timesheet.rates%']
|
||||||
|
|
||||||
|
App\Timesheet\RateCalculator\RateCalculatorMode:
|
||||||
|
factory: ['@App\Timesheet\RateCalculator\RateCalculatorFactory', getRateCalculatorMode]
|
||||||
|
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
# SECURITY & VOTER
|
# SECURITY & VOTER
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
|||||||
@@ -969,11 +969,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/DataFixtures/TimesheetFixtures.php
|
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\\.$#"
|
message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:createPermissionParameter\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -449,6 +449,11 @@ final class SystemConfigurationController extends AbstractController
|
|||||||
->setType(TextType::class)
|
->setType(TextType::class)
|
||||||
->setConstraints([new NotBlank()])
|
->setConstraints([new NotBlank()])
|
||||||
->setTranslationDomain('system-configuration'),
|
->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,
|
$authentication,
|
||||||
(new SystemConfigurationModel('customer'))
|
(new SystemConfigurationModel('customer'))
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ use App\Entity\Tag;
|
|||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\UserPreference;
|
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\Fixture;
|
||||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||||
use Doctrine\Persistence\ObjectManager;
|
use Doctrine\Persistence\ObjectManager;
|
||||||
@@ -60,10 +61,14 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
|||||||
$allUser = $this->getAllUsers($manager);
|
$allUser = $this->getAllUsers($manager);
|
||||||
$faker = Factory::create();
|
$faker = Factory::create();
|
||||||
$all = 0;
|
$all = 0;
|
||||||
|
$calculator = new ClassicRateCalculator();
|
||||||
|
|
||||||
foreach ($allUser as $user) {
|
foreach ($allUser as $user) {
|
||||||
// reload, because the manager might have been cleared
|
// reload, because the manager might have been cleared
|
||||||
$user = $manager->find(User::class, $user->getId());
|
$user = $manager->find(User::class, $user->getId());
|
||||||
|
if ($user === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// random amount of timesheet entries for every user
|
// random amount of timesheet entries for every user
|
||||||
$timesheetForUser = rand(self::MIN_TIMESHEETS_PER_USER, self::MAX_TIMESHEETS_PER_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(
|
$entry = $this->createTimesheetEntry(
|
||||||
|
$calculator,
|
||||||
$user,
|
$user,
|
||||||
$activities[array_rand($activities)],
|
$activities[array_rand($activities)],
|
||||||
$projects[array_rand($projects)],
|
$projects[array_rand($projects)],
|
||||||
@@ -98,6 +104,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
|||||||
// create active records
|
// create active records
|
||||||
if ($all % 3 === 0) {
|
if ($all % 3 === 0) {
|
||||||
$entry = $this->createTimesheetEntry(
|
$entry = $this->createTimesheetEntry(
|
||||||
|
$calculator,
|
||||||
$user,
|
$user,
|
||||||
$activities[array_rand($activities)],
|
$activities[array_rand($activities)],
|
||||||
$projects[array_rand($projects)],
|
$projects[array_rand($projects)],
|
||||||
@@ -208,7 +215,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
|||||||
return $this->findRandom($manager, Activity::class, 50);
|
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 = $this->getRandomFirstDay();
|
||||||
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');
|
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');
|
||||||
@@ -227,7 +234,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
|||||||
|
|
||||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||||
$hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
$hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||||
$rate = Util::calculateRate($hourlyRate, $duration);
|
$rate = $calculatorMode->calculateRate($hourlyRate, $duration);
|
||||||
|
|
||||||
$entry->setEnd($end);
|
$entry->setEnd($end);
|
||||||
$entry->setRate($rate);
|
$entry->setRate($rate);
|
||||||
|
|||||||
@@ -369,6 +369,10 @@ final class Configuration implements ConfigurationInterface
|
|||||||
->booleanNode('upload_twig')
|
->booleanNode('upload_twig')
|
||||||
->defaultFalse()
|
->defaultFalse()
|
||||||
->end()
|
->end()
|
||||||
|
->enumNode('rounding_mode')
|
||||||
|
->values(['decimal', 'classic'])
|
||||||
|
->defaultValue('classic')
|
||||||
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ namespace App\Invoice\Calculator;
|
|||||||
use App\Invoice\InvoiceItem;
|
use App\Invoice\InvoiceItem;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Invoice\TaxRow;
|
use App\Invoice\TaxRow;
|
||||||
use App\Timesheet\Util;
|
|
||||||
|
|
||||||
abstract class AbstractCalculator
|
abstract class AbstractCalculator
|
||||||
{
|
{
|
||||||
@@ -42,10 +41,10 @@ abstract class AbstractCalculator
|
|||||||
if (\count($this->cached) === 0) {
|
if (\count($this->cached) === 0) {
|
||||||
foreach ($this->calculateEntries() as $entry) {
|
foreach ($this->calculateEntries() as $entry) {
|
||||||
if (!$entry->isFixedRate() && $entry->getHourlyRate() !== null && $entry->getHourlyRate() > 0) {
|
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
|
// when merging many entries, we might run into rounding issues
|
||||||
// so we have to recalculate the hourly rate here
|
// 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;
|
$this->cached[] = $entry;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
|
|||||||
|
|
||||||
$values = [
|
$values = [
|
||||||
$prefix . 'id' => $customer->getId(),
|
$prefix . 'id' => $customer->getId(),
|
||||||
$prefix . 'address' => $customer->getFormattedAddress() ?? '',
|
$prefix . 'address' => $customer->getFormattedAddress() ?? '', // deprecated since 2.44
|
||||||
$prefix . 'address_line1' => $customer->getAddressLine1() ?? '',
|
$prefix . 'address_line1' => $customer->getAddressLine1() ?? '',
|
||||||
$prefix . 'address_line2' => $customer->getAddressLine2() ?? '',
|
$prefix . 'address_line2' => $customer->getAddressLine2() ?? '',
|
||||||
$prefix . 'address_line3' => $customer->getAddressLine3() ?? '',
|
$prefix . 'address_line3' => $customer->getAddressLine3() ?? '',
|
||||||
|
|||||||
@@ -62,12 +62,12 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
|||||||
'invoice.subtotal_plain' => $subtotal,
|
'invoice.subtotal_plain' => $subtotal,
|
||||||
|
|
||||||
'template.name' => $template->getName() ?? '',
|
'template.name' => $template->getName() ?? '',
|
||||||
'template.company' => $template->getCompany() ?? '',
|
'template.company' => $template->getCompany() ?? '', // deprecated since 2.45
|
||||||
'template.address' => $template->getAddress() ?? '',
|
'template.address' => $template->getAddress() ?? '',
|
||||||
'template.title' => $template->getTitle() ?? '',
|
'template.title' => $template->getTitle() ?? '',
|
||||||
'template.payment_terms' => $template->getPaymentTerms() ?? '',
|
'template.payment_terms' => $template->getPaymentTerms() ?? '',
|
||||||
'template.due_days' => $template->getDueDays(),
|
'template.due_days' => $template->getDueDays(),
|
||||||
'template.vat_id' => $template->getVatId() ?? '',
|
'template.vat_id' => $template->getVatId() ?? '', // deprecated since 2.45
|
||||||
'template.contact' => $template->getContact() ?? '',
|
'template.contact' => $template->getContact() ?? '',
|
||||||
'template.country' => null,
|
'template.country' => null,
|
||||||
'template.country_name' => null,
|
'template.country_name' => null,
|
||||||
@@ -105,8 +105,8 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
|||||||
if ($seller !== null) {
|
if ($seller !== null) {
|
||||||
$country = $seller->getCountry();
|
$country = $seller->getCountry();
|
||||||
if ($country !== null) {
|
if ($country !== null) {
|
||||||
$values['template.country'] = $country;
|
$values['template.country'] = $country; // deprecated since 2.45
|
||||||
$values['template.country_name'] = Countries::getName($country, $language);
|
$values['template.country_name'] = Countries::getName($country, $language); // deprecated since 2.45
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,14 +116,10 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
|||||||
if ($begin !== null) {
|
if ($begin !== null) {
|
||||||
$values = array_merge($values, [
|
$values = array_merge($values, [
|
||||||
'query.day' => $begin->format('d'),
|
'query.day' => $begin->format('d'),
|
||||||
// @deprecated - but impossible to delete
|
'query.month' => $formatter->getFormattedMonthName($begin), // @deprecated - but impossible to delete
|
||||||
'query.month' => $formatter->getFormattedMonthName($begin),
|
'query.month_number' => $begin->format('m'), // @deprecated - but impossible to delete
|
||||||
// @deprecated - but impossible to delete
|
'query.year' => $begin->format('Y'), // @deprecated - but impossible to delete
|
||||||
'query.month_number' => $begin->format('m'),
|
'query.begin' => $formatter->getFormattedDateTime($begin), // @deprecated - but impossible to delete
|
||||||
// @deprecated - but impossible to delete
|
|
||||||
'query.year' => $begin->format('Y'),
|
|
||||||
// @deprecated - but impossible to delete
|
|
||||||
'query.begin' => $formatter->getFormattedDateTime($begin),
|
|
||||||
'query.begin_process' => $begin->format(self::DATE_PROCESS_FORMAT), // since 2.14
|
'query.begin_process' => $begin->format(self::DATE_PROCESS_FORMAT), // since 2.14
|
||||||
'query.begin_day' => $begin->format('d'),
|
'query.begin_day' => $begin->format('d'),
|
||||||
'query.begin_month' => $formatter->getFormattedMonthName($begin),
|
'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\InvoiceModelActivityHydrator;
|
||||||
use App\Invoice\Hydrator\InvoiceModelCustomerHydrator;
|
use App\Invoice\Hydrator\InvoiceModelCustomerHydrator;
|
||||||
use App\Invoice\Hydrator\InvoiceModelDefaultHydrator;
|
use App\Invoice\Hydrator\InvoiceModelDefaultHydrator;
|
||||||
|
use App\Invoice\Hydrator\InvoiceModelIssuerHydrator;
|
||||||
use App\Invoice\Hydrator\InvoiceModelProjectHydrator;
|
use App\Invoice\Hydrator\InvoiceModelProjectHydrator;
|
||||||
use App\Invoice\Hydrator\InvoiceModelUserHydrator;
|
use App\Invoice\Hydrator\InvoiceModelUserHydrator;
|
||||||
use App\Project\ProjectStatisticService;
|
use App\Project\ProjectStatisticService;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
use Symfony\Component\DependencyInjection\Attribute\Exclude;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,12 +70,14 @@ final class InvoiceModel
|
|||||||
ActivityStatisticService $activityStatistic,
|
ActivityStatisticService $activityStatistic,
|
||||||
private readonly Customer $customer,
|
private readonly Customer $customer,
|
||||||
private readonly InvoiceTemplate $template,
|
private readonly InvoiceTemplate $template,
|
||||||
|
private readonly RateCalculatorMode $rateCalculatorMode
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
$this->invoiceDate = new \DateTimeImmutable();
|
$this->invoiceDate = new \DateTimeImmutable();
|
||||||
$this->formatter = $formatter;
|
$this->formatter = $formatter;
|
||||||
$this->addModelHydrator(new InvoiceModelDefaultHydrator());
|
$this->addModelHydrator(new InvoiceModelDefaultHydrator());
|
||||||
$this->addModelHydrator(new InvoiceModelCustomerHydrator($customerStatistic));
|
$this->addModelHydrator(new InvoiceModelCustomerHydrator($customerStatistic));
|
||||||
|
$this->addModelHydrator(new InvoiceModelIssuerHydrator());
|
||||||
$this->addModelHydrator(new InvoiceModelProjectHydrator($projectStatistic));
|
$this->addModelHydrator(new InvoiceModelProjectHydrator($projectStatistic));
|
||||||
$this->addModelHydrator(new InvoiceModelActivityHydrator($activityStatistic));
|
$this->addModelHydrator(new InvoiceModelActivityHydrator($activityStatistic));
|
||||||
$this->addModelHydrator(new InvoiceModelUserHydrator());
|
$this->addModelHydrator(new InvoiceModelUserHydrator());
|
||||||
@@ -207,6 +211,11 @@ final class InvoiceModel
|
|||||||
return $this->calculator;
|
return $this->calculator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRateCalculatorMode(): RateCalculatorMode
|
||||||
|
{
|
||||||
|
return $this->rateCalculatorMode;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the user currently creating the invoice.
|
* Returns the user currently creating the invoice.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,19 +15,21 @@ use App\Entity\Customer;
|
|||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
use App\Project\ProjectStatisticService;
|
use App\Project\ProjectStatisticService;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||||
|
|
||||||
final class InvoiceModelFactory
|
final class InvoiceModelFactory
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CustomerStatisticService $customerStatisticService,
|
private readonly CustomerStatisticService $customerStatisticService,
|
||||||
private readonly ProjectStatisticService $projectStatisticService,
|
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
|
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);
|
$model->setQuery($query);
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ final class ServiceInvoice
|
|||||||
private array $invoiceItemRepositories = [];
|
private array $invoiceItemRepositories = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private InvoiceDocumentRepository $documents,
|
private readonly InvoiceDocumentRepository $documents,
|
||||||
private FileHelper $fileHelper,
|
private readonly FileHelper $fileHelper,
|
||||||
private InvoiceRepository $invoiceRepository,
|
private readonly InvoiceRepository $invoiceRepository,
|
||||||
private LocaleService $formatter,
|
private readonly LocaleService $formatter,
|
||||||
private InvoiceModelFactory $invoiceModelFactory
|
private readonly InvoiceModelFactory $invoiceModelFactory
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use App\Timesheet\RateServiceInterface;
|
|||||||
*/
|
*/
|
||||||
final class RateCalculator implements CalculatorInterface
|
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\Timesheet;
|
||||||
use App\Entity\UserPreference;
|
use App\Entity\UserPreference;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
|
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation to calculate the rate for a timesheet record.
|
* Implementation to calculate the rate for a timesheet record.
|
||||||
*/
|
*/
|
||||||
final class RateService implements RateServiceInterface
|
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;
|
$totalInternalRate = 0;
|
||||||
|
|
||||||
if (null !== $record->getDuration()) {
|
if (null !== $record->getDuration()) {
|
||||||
$totalRate = Util::calculateRate($factoredHourlyRate, $record->getDuration());
|
$totalRate = $this->calculatorMode->calculateRate($factoredHourlyRate, $record->getDuration());
|
||||||
$totalInternalRate = Util::calculateRate($factoredInternalRate, $record->getDuration());
|
$totalInternalRate = $this->calculatorMode->calculateRate($factoredInternalRate, $record->getDuration());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Rate($totalRate, $totalInternalRate, $factoredHourlyRate, null);
|
return new Rate($totalRate, $totalInternalRate, $factoredHourlyRate, null);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
namespace App\Timesheet;
|
namespace App\Timesheet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A static helper class for re-usable functionality.
|
* @deprecated use RateCalculatorMode instead
|
||||||
*/
|
*/
|
||||||
final class Util
|
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
|
public static function decimalizeDuration(int $seconds): int
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ use App\Entity\ProjectRate;
|
|||||||
use App\Entity\RateInterface;
|
use App\Entity\RateInterface;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
|
||||||
/**
|
|
||||||
* @group integration
|
|
||||||
*/
|
|
||||||
trait RateControllerTestTrait
|
trait RateControllerTestTrait
|
||||||
{
|
{
|
||||||
abstract protected function getRateUrl(?int $id = 1, ?int $rateId = null): string;
|
abstract protected function getRateUrl(?int $id = 1, ?int $rateId = null): string;
|
||||||
|
|||||||
@@ -426,8 +426,8 @@ class TimesheetControllerTest extends APIControllerBaseTestCase
|
|||||||
'exported' => true,
|
'exported' => true,
|
||||||
'metaFields' => [],
|
'metaFields' => [],
|
||||||
'hourlyRate' => 137.21,
|
'hourlyRate' => 137.21,
|
||||||
'rate' => 1772.75, // 12,92 * 137,21
|
'rate' => 1772.2958,
|
||||||
'internalRate' => 1772.75,
|
'internalRate' => 1772.2958,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($expected as $key => $value) {
|
foreach ($expected as $key => $value) {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ use App\Form\Type\TagsType;
|
|||||||
use App\Tests\DataFixtures\TagFixtures;
|
use App\Tests\DataFixtures\TagFixtures;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
use App\Timesheet\DateTimeFactory;
|
use App\Timesheet\DateTimeFactory;
|
||||||
use App\Timesheet\Util;
|
|
||||||
use PHPUnit\Framework\Attributes\Group;
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
|
|
||||||
#[Group('integration')]
|
#[Group('integration')]
|
||||||
@@ -414,7 +413,7 @@ class TimesheetTeamControllerTest extends AbstractControllerBaseTestCase
|
|||||||
self::assertCount(3, $timesheet->getTags());
|
self::assertCount(3, $timesheet->getTags());
|
||||||
self::assertEquals($newUser->getId(), $timesheet->getUser()->getId());
|
self::assertEquals($newUser->getId(), $timesheet->getUser()->getId());
|
||||||
self::assertTrue($timesheet->isExported());
|
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\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\UserPreference;
|
use App\Entity\UserPreference;
|
||||||
use App\Timesheet\Util;
|
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||||
|
use App\Timesheet\RateCalculator\RateCalculatorMode;
|
||||||
use Doctrine\Persistence\ObjectManager;
|
use Doctrine\Persistence\ObjectManager;
|
||||||
use Faker\Factory;
|
use Faker\Factory;
|
||||||
|
|
||||||
@@ -205,6 +206,7 @@ final class TimesheetFixtures implements TestFixture
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$manager->flush();
|
$manager->flush();
|
||||||
|
$calculator = new ClassicRateCalculator();
|
||||||
|
|
||||||
for ($i = 0; $i < $this->amount; $i++) {
|
for ($i = 0; $i < $this->amount; $i++) {
|
||||||
$description = $faker->text();
|
$description = $faker->text();
|
||||||
@@ -217,6 +219,9 @@ final class TimesheetFixtures implements TestFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
$user = $users[array_rand($users)];
|
$user = $users[array_rand($users)];
|
||||||
|
if ($user === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$activity = $activities[array_rand($activities)];
|
$activity = $activities[array_rand($activities)];
|
||||||
$project = $activity->getProject();
|
$project = $activity->getProject();
|
||||||
|
|
||||||
@@ -225,6 +230,7 @@ final class TimesheetFixtures implements TestFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
$timesheet = $this->createTimesheetEntry(
|
$timesheet = $this->createTimesheetEntry(
|
||||||
|
$calculator,
|
||||||
$user,
|
$user,
|
||||||
$activity,
|
$activity,
|
||||||
$project,
|
$project,
|
||||||
@@ -244,12 +250,16 @@ final class TimesheetFixtures implements TestFixture
|
|||||||
$activity = $activities[array_rand($activities)];
|
$activity = $activities[array_rand($activities)];
|
||||||
$project = $activity->getProject();
|
$project = $activity->getProject();
|
||||||
$user = $users[array_rand($users)];
|
$user = $users[array_rand($users)];
|
||||||
|
if ($user === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (null === $project) {
|
if ($project === null) {
|
||||||
$project = $projects[array_rand($projects)];
|
$project = $projects[array_rand($projects)];
|
||||||
}
|
}
|
||||||
|
|
||||||
$timesheet = $this->createTimesheetEntry(
|
$timesheet = $this->createTimesheetEntry(
|
||||||
|
$calculator,
|
||||||
$user,
|
$user,
|
||||||
$activity,
|
$activity,
|
||||||
$project,
|
$project,
|
||||||
@@ -310,17 +320,25 @@ final class TimesheetFixtures implements TestFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \DateTime $start
|
|
||||||
* @param array<Tag> $tagArray
|
* @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 = clone $start;
|
||||||
$end = $end->modify('+ ' . (rand(1, 86400)) . ' seconds');
|
$end = $end->modify('+ ' . (rand(1, 86400)) . ' seconds');
|
||||||
|
|
||||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||||
$hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
$hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||||
$rate = Util::calculateRate($hourlyRate, $duration);
|
$rate = $calculatorMode->calculateRate($hourlyRate, $duration);
|
||||||
|
|
||||||
$entry = new Timesheet();
|
$entry = new Timesheet();
|
||||||
$entry->setActivity($activity);
|
$entry->setActivity($activity);
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ class ConfigurationTest extends TestCase
|
|||||||
],
|
],
|
||||||
'number_format' => '{Y}/{cy,3}',
|
'number_format' => '{Y}/{cy,3}',
|
||||||
'upload_twig' => false,
|
'upload_twig' => false,
|
||||||
|
'rounding_mode' => 'classic',
|
||||||
],
|
],
|
||||||
'export' => [
|
'export' => [
|
||||||
'documents' => [
|
'documents' => [
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ class HtmlRendererTest extends AbstractRendererTestCase
|
|||||||
self::assertFalse($sut->isInternal());
|
self::assertFalse($sut->isInternal());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Group('legacy')]
|
||||||
* @group legacy
|
|
||||||
*/
|
|
||||||
public function testLegacy(): void
|
public function testLegacy(): void
|
||||||
{
|
{
|
||||||
$sut = $this->getAbstractRenderer();
|
$sut = $this->getAbstractRenderer();
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ class PdfRendererTest extends AbstractRendererTestCase
|
|||||||
self::assertFalse($sut->isInternal());
|
self::assertFalse($sut->isInternal());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Group('legacy')]
|
||||||
* @group legacy
|
|
||||||
*/
|
|
||||||
public function testLegacy(): void
|
public function testLegacy(): void
|
||||||
{
|
{
|
||||||
$sut = $this->getAbstractRenderer();
|
$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',
|
||||||
'invoice.subtotal_nc',
|
'invoice.subtotal_nc',
|
||||||
'invoice.subtotal_plain',
|
'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.name',
|
||||||
'template.company',
|
'template.company',
|
||||||
'template.country',
|
'template.country',
|
||||||
|
|||||||
@@ -54,9 +54,17 @@ trait RendererTestTrait
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string $classname
|
||||||
|
*/
|
||||||
protected function getAbstractRenderer(string $classname): AbstractRenderer
|
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
|
protected function getFormatter(): InvoiceFormatter
|
||||||
|
|||||||
@@ -10,11 +10,12 @@
|
|||||||
namespace App\Tests\Mocks;
|
namespace App\Tests\Mocks;
|
||||||
|
|
||||||
use PHPUnit\Framework\MockObject\MockBuilder;
|
use PHPUnit\Framework\MockObject\MockBuilder;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
abstract class AbstractMockFactory
|
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;
|
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
|
protected function getMockBuilder(string $className): MockBuilder
|
||||||
{
|
{
|
||||||
return new MockBuilder($this->testCase, $className);
|
return new MockBuilder($this->testCase, $className);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Activity\ActivityStatisticService;
|
|||||||
use App\Customer\CustomerStatisticService;
|
use App\Customer\CustomerStatisticService;
|
||||||
use App\Invoice\InvoiceModelFactory;
|
use App\Invoice\InvoiceModelFactory;
|
||||||
use App\Project\ProjectStatisticService;
|
use App\Project\ProjectStatisticService;
|
||||||
|
use App\Timesheet\RateCalculator\DecimalRateCalculator;
|
||||||
|
|
||||||
class InvoiceModelFactoryFactory extends AbstractMockFactory
|
class InvoiceModelFactoryFactory extends AbstractMockFactory
|
||||||
{
|
{
|
||||||
@@ -24,7 +25,8 @@ class InvoiceModelFactoryFactory extends AbstractMockFactory
|
|||||||
$projectStatistic = $this->getMockBuilder(ProjectStatisticService::class)->disableOriginalConstructor()->getMock();
|
$projectStatistic = $this->getMockBuilder(ProjectStatisticService::class)->disableOriginalConstructor()->getMock();
|
||||||
/** @var ActivityStatisticService $activityStatistic */
|
/** @var ActivityStatisticService $activityStatistic */
|
||||||
$activityStatistic = $this->getMockBuilder(ActivityStatisticService::class)->disableOriginalConstructor()->getMock();
|
$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
|
* @param array<mixed> $settings
|
||||||
* @return SystemConfiguration
|
|
||||||
*/
|
*/
|
||||||
public static function create(ConfigLoaderInterface $repository, array $settings): SystemConfiguration
|
public static function create(ConfigLoaderInterface $repository, array $settings): SystemConfiguration
|
||||||
{
|
{
|
||||||
@@ -26,7 +25,6 @@ class SystemConfigurationFactory
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<mixed> $settings
|
* @param array<mixed> $settings
|
||||||
* @return SystemConfiguration
|
|
||||||
*/
|
*/
|
||||||
public static function createStub(array $settings = []): SystemConfiguration
|
public static function createStub(array $settings = []): SystemConfiguration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use App\Entity\ProjectRate;
|
|||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\UserPreference;
|
use App\Entity\UserPreference;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Tests\Mocks\RateServiceFactory;
|
||||||
use App\Timesheet\Calculator\RateCalculator;
|
use App\Timesheet\Calculator\RateCalculator;
|
||||||
use App\Timesheet\RateService;
|
use App\Timesheet\RateService;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
@@ -28,14 +28,11 @@ use PHPUnit\Framework\TestCase;
|
|||||||
#[CoversClass(RateCalculator::class)]
|
#[CoversClass(RateCalculator::class)]
|
||||||
class RateCalculatorTest extends TestCase
|
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();
|
$factory = new RateServiceFactory($this);
|
||||||
if (!empty($rates)) {
|
|
||||||
$mock->expects($this->any())->method('findMatchingRates')->willReturn($rates);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $mock;
|
return $factory->create($rules, $rates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertRateByTimesheetHourlyRate(int $duration, float $hourlyRate, float $rate): void
|
private function assertRateByTimesheetHourlyRate(int $duration, float $hourlyRate, float $rate): void
|
||||||
@@ -47,7 +44,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
$record->setActivity(new Activity());
|
$record->setActivity(new Activity());
|
||||||
$record->setUser($this->getTestUser());
|
$record->setUser($this->getTestUser());
|
||||||
|
|
||||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
$sut = new RateCalculator($this->getRateService());
|
||||||
$sut->calculate($record, []);
|
$sut->calculate($record, []);
|
||||||
self::assertEquals($rate, $record->getRate());
|
self::assertEquals($rate, $record->getRate());
|
||||||
}
|
}
|
||||||
@@ -55,9 +52,9 @@ class RateCalculatorTest extends TestCase
|
|||||||
public function testCalculateWithTimesheetHourlyRate(): void
|
public function testCalculateWithTimesheetHourlyRate(): void
|
||||||
{
|
{
|
||||||
$this->assertRateByTimesheetHourlyRate(1800, 100, 50);
|
$this->assertRateByTimesheetHourlyRate(1800, 100, 50);
|
||||||
$this->assertRateByTimesheetHourlyRate(400, 100, 11);
|
$this->assertRateByTimesheetHourlyRate(400, 100, 11.1111);
|
||||||
$this->assertRateByTimesheetHourlyRate(1234, 100, 34);
|
$this->assertRateByTimesheetHourlyRate(1234, 100, 34.2778);
|
||||||
$this->assertRateByTimesheetHourlyRate(2739, 100, 76);
|
$this->assertRateByTimesheetHourlyRate(2739, 100, 76.0833);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCalculateWithTimesheetFixedRate(): void
|
public function testCalculateWithTimesheetFixedRate(): void
|
||||||
@@ -71,7 +68,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
$record->setActivity(new Activity());
|
$record->setActivity(new Activity());
|
||||||
$record->setUser($this->getTestUser());
|
$record->setUser($this->getTestUser());
|
||||||
|
|
||||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
$sut = new RateCalculator($this->getRateService());
|
||||||
$sut->calculate($record, []);
|
$sut->calculate($record, []);
|
||||||
self::assertEquals(10, $record->getRate());
|
self::assertEquals(10, $record->getRate());
|
||||||
}
|
}
|
||||||
@@ -112,22 +109,22 @@ class RateCalculatorTest extends TestCase
|
|||||||
|
|
||||||
#[DataProvider('getRateTestData')]
|
#[DataProvider('getRateTestData')]
|
||||||
public function testRates(
|
public function testRates(
|
||||||
$expectedRate,
|
float $expectedRate,
|
||||||
$expectedInternalRate,
|
float $expectedInternalRate,
|
||||||
$duration,
|
int $duration,
|
||||||
$userRate,
|
float $userRate,
|
||||||
$userInternalRate,
|
?float $userInternalRate,
|
||||||
$timesheetHourly,
|
?float $timesheetHourly,
|
||||||
$timesheetFixed,
|
?float $timesheetFixed,
|
||||||
$activityRate,
|
?float $activityRate,
|
||||||
$activityInternal,
|
?float $activityInternal,
|
||||||
$activityIsFixed,
|
bool $activityIsFixed,
|
||||||
$projectRate,
|
?float $projectRate,
|
||||||
$projectInternal,
|
?float $projectInternal,
|
||||||
$projectIsFixed,
|
bool $projectIsFixed,
|
||||||
$customerRate,
|
?float $customerRate,
|
||||||
$customerInternal,
|
?float $customerInternal,
|
||||||
$customerIsFixed
|
bool $customerIsFixed
|
||||||
) {
|
) {
|
||||||
$customer = new Customer('foo');
|
$customer = new Customer('foo');
|
||||||
|
|
||||||
@@ -178,7 +175,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
$rates[] = $rate;
|
$rates[] = $rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock($rates)));
|
$sut = new RateCalculator($this->getRateService([], $rates));
|
||||||
$sut->calculate($timesheet, []);
|
$sut->calculate($timesheet, []);
|
||||||
self::assertEquals($expectedRate, $timesheet->getRate());
|
self::assertEquals($expectedRate, $timesheet->getRate());
|
||||||
self::assertEquals($expectedInternalRate, $timesheet->getInternalRate());
|
self::assertEquals($expectedInternalRate, $timesheet->getInternalRate());
|
||||||
@@ -207,7 +204,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
|
|
||||||
self::assertEquals(0, $record->getRate());
|
self::assertEquals(0, $record->getRate());
|
||||||
|
|
||||||
$sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock()));
|
$sut = new RateCalculator($this->getRateService());
|
||||||
$sut->calculate($record, []);
|
$sut->calculate($record, []);
|
||||||
self::assertEquals(0, $record->getRate());
|
self::assertEquals(0, $record->getRate());
|
||||||
}
|
}
|
||||||
@@ -216,7 +213,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
* Uses the hourly rate from user_preferences to calculate the rate.
|
* Uses the hourly rate from user_preferences to calculate the rate.
|
||||||
*/
|
*/
|
||||||
#[DataProvider('getRuleDefinitions')]
|
#[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'));
|
$end = new \DateTime('12:00:00', new \DateTimeZone('UTC'));
|
||||||
$start = clone $end;
|
$start = clone $end;
|
||||||
@@ -232,7 +229,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
|
|
||||||
$record->setEnd($end);
|
$record->setEnd($end);
|
||||||
|
|
||||||
$sut = new RateCalculator(new RateService($rules, $this->getRateRepositoryMock()));
|
$sut = new RateCalculator($this->getRateService($rules));
|
||||||
$sut->calculate($record, []);
|
$sut->calculate($record, []);
|
||||||
|
|
||||||
self::assertEquals($expectedRate, $record->getRate());
|
self::assertEquals($expectedRate, $record->getRate());
|
||||||
@@ -247,7 +244,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
[
|
[
|
||||||
31837, // 31824 = 8,84
|
31837, // 31824 = 8,84
|
||||||
[],
|
[],
|
||||||
663
|
663.2708
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
31837, // 31824 = 8,84
|
31837, // 31824 = 8,84
|
||||||
@@ -261,7 +258,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
'factor' => 1.5
|
'factor' => 1.5
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
1326 // 8,84 * 75 (see user) * 2
|
1326.5417
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
31837, // 31824 = 8,84
|
31837, // 31824 = 8,84
|
||||||
@@ -275,7 +272,7 @@ class RateCalculatorTest extends TestCase
|
|||||||
'factor' => 1.5
|
'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\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\UserPreference;
|
use App\Entity\UserPreference;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Tests\Mocks\RateServiceFactory;
|
||||||
use App\Timesheet\RateService;
|
use App\Timesheet\RateService;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
@@ -27,14 +27,11 @@ use PHPUnit\Framework\TestCase;
|
|||||||
#[CoversClass(RateService::class)]
|
#[CoversClass(RateService::class)]
|
||||||
class RateServiceTest extends TestCase
|
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();
|
$factory = new RateServiceFactory($this);
|
||||||
if (!empty($rates)) {
|
|
||||||
$mock->expects($this->any())->method('findMatchingRates')->willReturn($rates);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $mock;
|
return $factory->create($rules, $rates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function createDateTime(?string $datetime = null): \DateTime
|
private static function createDateTime(?string $datetime = null): \DateTime
|
||||||
@@ -51,7 +48,7 @@ class RateServiceTest extends TestCase
|
|||||||
$record->setActivity(new Activity());
|
$record->setActivity(new Activity());
|
||||||
$record->setUser($this->getTestUser());
|
$record->setUser($this->getTestUser());
|
||||||
|
|
||||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
$sut = $this->getSut();
|
||||||
$rate = $sut->calculate($record);
|
$rate = $sut->calculate($record);
|
||||||
self::assertEquals(50, $rate->getRate());
|
self::assertEquals(50, $rate->getRate());
|
||||||
}
|
}
|
||||||
@@ -67,7 +64,7 @@ class RateServiceTest extends TestCase
|
|||||||
$record->setActivity(new Activity());
|
$record->setActivity(new Activity());
|
||||||
$record->setUser($this->getTestUser());
|
$record->setUser($this->getTestUser());
|
||||||
|
|
||||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
$sut = $this->getSut();
|
||||||
$rate = $sut->calculate($record);
|
$rate = $sut->calculate($record);
|
||||||
self::assertEquals(10, $rate->getRate());
|
self::assertEquals(10, $rate->getRate());
|
||||||
}
|
}
|
||||||
@@ -108,22 +105,22 @@ class RateServiceTest extends TestCase
|
|||||||
|
|
||||||
#[DataProvider('getRateTestData')]
|
#[DataProvider('getRateTestData')]
|
||||||
public function testRates(
|
public function testRates(
|
||||||
$expectedRate,
|
float $expectedRate,
|
||||||
$expectedInternalRate,
|
float $expectedInternalRate,
|
||||||
$duration,
|
int $duration,
|
||||||
$userRate,
|
float $userRate,
|
||||||
$userInternalRate,
|
?float $userInternalRate,
|
||||||
$timesheetHourly,
|
?float $timesheetHourly,
|
||||||
$timesheetFixed,
|
?float $timesheetFixed,
|
||||||
$activityRate,
|
?float $activityRate,
|
||||||
$activityInternal,
|
?float $activityInternal,
|
||||||
$activityIsFixed,
|
bool $activityIsFixed,
|
||||||
$projectRate,
|
?float $projectRate,
|
||||||
$projectInternal,
|
?float $projectInternal,
|
||||||
$projectIsFixed,
|
bool $projectIsFixed,
|
||||||
$customerRate,
|
?float $customerRate,
|
||||||
$customerInternal,
|
?float $customerInternal,
|
||||||
$customerIsFixed
|
bool $customerIsFixed
|
||||||
): void {
|
): void {
|
||||||
$customer = new Customer('foo');
|
$customer = new Customer('foo');
|
||||||
|
|
||||||
@@ -174,7 +171,7 @@ class RateServiceTest extends TestCase
|
|||||||
$rates[] = $rate;
|
$rates[] = $rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sut = new RateService([], $this->getRateRepositoryMock($rates));
|
$sut = $this->getSut([], $rates);
|
||||||
$rate = $sut->calculate($timesheet);
|
$rate = $sut->calculate($timesheet);
|
||||||
self::assertEquals($expectedRate, $rate->getRate());
|
self::assertEquals($expectedRate, $rate->getRate());
|
||||||
self::assertEquals($expectedInternalRate, $rate->getInternalRate());
|
self::assertEquals($expectedInternalRate, $rate->getInternalRate());
|
||||||
@@ -203,7 +200,7 @@ class RateServiceTest extends TestCase
|
|||||||
|
|
||||||
self::assertEquals(0, $record->getRate());
|
self::assertEquals(0, $record->getRate());
|
||||||
|
|
||||||
$sut = new RateService([], $this->getRateRepositoryMock());
|
$sut = $this->getSut();
|
||||||
$rate = $sut->calculate($record);
|
$rate = $sut->calculate($record);
|
||||||
self::assertEquals(0, $rate->getRate());
|
self::assertEquals(0, $rate->getRate());
|
||||||
}
|
}
|
||||||
@@ -212,7 +209,7 @@ class RateServiceTest extends TestCase
|
|||||||
* Uses the hourly rate from user_preferences to calculate the rate.
|
* Uses the hourly rate from user_preferences to calculate the rate.
|
||||||
*/
|
*/
|
||||||
#[DataProvider('getRuleDefinitions')]
|
#[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');
|
$end = self::createDateTime('12:00:00');
|
||||||
$start = clone $end;
|
$start = clone $end;
|
||||||
@@ -228,7 +225,7 @@ class RateServiceTest extends TestCase
|
|||||||
|
|
||||||
$record->setEnd($end);
|
$record->setEnd($end);
|
||||||
|
|
||||||
$sut = new RateService($rules, $this->getRateRepositoryMock());
|
$sut = $this->getSut($rules);
|
||||||
$rate = $sut->calculate($record);
|
$rate = $sut->calculate($record);
|
||||||
|
|
||||||
self::assertEquals($expectedRate, $rate->getRate());
|
self::assertEquals($expectedRate, $rate->getRate());
|
||||||
@@ -243,7 +240,7 @@ class RateServiceTest extends TestCase
|
|||||||
[
|
[
|
||||||
31837,
|
31837,
|
||||||
[],
|
[],
|
||||||
663
|
663.2708
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
31837,
|
31837,
|
||||||
@@ -257,7 +254,7 @@ class RateServiceTest extends TestCase
|
|||||||
'factor' => 1.5
|
'factor' => 1.5
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
1326 // 8,84 * 75 (see user) * 2
|
1326.5417
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
31837,
|
31837,
|
||||||
@@ -271,7 +268,7 @@ class RateServiceTest extends TestCase
|
|||||||
'factor' => 1.5
|
'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 App\Timesheet\Util;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
#[Group('legacy')]
|
||||||
#[CoversClass(Util::class)]
|
#[CoversClass(Util::class)]
|
||||||
class UtilTest extends TestCase
|
class UtilTest extends TestCase
|
||||||
{
|
{
|
||||||
|
#[Group('legacy')]
|
||||||
#[DataProvider('getRateCalculationData')]
|
#[DataProvider('getRateCalculationData')]
|
||||||
public function testCalculateRate(float $hourlyRate, int $duration, float $expectedRate): void
|
public function testCalculateRate(float $hourlyRate, int $duration, float $expectedRate): void
|
||||||
{
|
{
|
||||||
@@ -42,6 +45,7 @@ class UtilTest extends TestCase
|
|||||||
yield [7360.99, 1234, 2502.74];
|
yield [7360.99, 1234, 2502.74];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Group('legacy')]
|
||||||
public function testCalculateRateWithRounding(): void
|
public function testCalculateRateWithRounding(): void
|
||||||
{
|
{
|
||||||
$total = 0.00;
|
$total = 0.00;
|
||||||
@@ -82,6 +86,7 @@ class UtilTest extends TestCase
|
|||||||
self::assertEqualsWithDelta(149176.3, $total, 0.00001);
|
self::assertEqualsWithDelta(149176.3, $total, 0.00001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Group('legacy')]
|
||||||
public function testDecimalDuration(): void
|
public function testDecimalDuration(): void
|
||||||
{
|
{
|
||||||
$inputs = [
|
$inputs = [
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use App\Project\ProjectStatisticService;
|
|||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
use App\Tests\Mocks\SystemConfigurationFactory;
|
use App\Tests\Mocks\SystemConfigurationFactory;
|
||||||
use App\Timesheet\Rate;
|
use App\Timesheet\Rate;
|
||||||
|
use App\Timesheet\RateCalculator\ClassicRateCalculator;
|
||||||
use App\Timesheet\RateService;
|
use App\Timesheet\RateService;
|
||||||
use App\Timesheet\RateServiceInterface;
|
use App\Timesheet\RateServiceInterface;
|
||||||
use App\Validator\Constraints\TimesheetBudgetUsed;
|
use App\Validator\Constraints\TimesheetBudgetUsed;
|
||||||
@@ -92,7 +93,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase
|
|||||||
$rateService = $this->createMock(RateServiceInterface::class);
|
$rateService = $this->createMock(RateServiceInterface::class);
|
||||||
$rateService->method('calculate')->willReturn($rate);
|
$rateService->method('calculate')->willReturn($rate);
|
||||||
} else {
|
} else {
|
||||||
$rateService = new RateService([], $timesheetRepository);
|
$rateService = new RateService([], $timesheetRepository, new ClassicRateCalculator());
|
||||||
}
|
}
|
||||||
|
|
||||||
$auth = $this->createMock(AuthorizationCheckerInterface::class);
|
$auth = $this->createMock(AuthorizationCheckerInterface::class);
|
||||||
|
|||||||
@@ -966,11 +966,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Controller/TimesheetTeamControllerTest.php
|
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\\.$#"
|
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
|
||||||
count: 2
|
count: 2
|
||||||
@@ -1411,61 +1406,31 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelActivityHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Hydrator/InvoiceModelActivityHydratorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelCustomerHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelDefaultHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelProjectHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Hydrator/InvoiceModelProjectHydratorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelUserHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Hydrator/InvoiceModelUserHydratorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\DateNumberGeneratorTest\\:\\:getSut\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1521,11 +1486,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Renderer/DebugRendererTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DebugRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1576,16 +1536,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Renderer/DebugRendererTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\OdsRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1621,11 +1571,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Invoice/Renderer/OdsRendererTest.php
|
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\\.$#"
|
message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1636,11 +1581,6 @@ parameters:
|
|||||||
count: 3
|
count: 3
|
||||||
path: Invoice/Renderer/PdfRendererTest.php
|
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\\.$#"
|
message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1651,11 +1591,6 @@ parameters:
|
|||||||
count: 3
|
count: 3
|
||||||
path: Invoice/Renderer/TwigRendererTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\XlsxRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1731,20 +1666,11 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Ldap/LdapManagerTest.php
|
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$#"
|
message: "#^Method App\\\\Tests\\\\Mocks\\\\AbstractMockFactory\\:\\:getMockBuilder\\(\\) return type with generic class PHPUnit\\\\Framework\\\\MockObject\\\\MockBuilder does not specify its types\\: TMockedClass$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Mocks/AbstractMockFactory.php
|
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\\.$#"
|
message: "#^Parameter \\#1 \\$dataDir of class App\\\\Utils\\\\FileHelper constructor expects string, string\\|false given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1935,116 +1861,16 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Timesheet/Calculator/DurationCalculatorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getRateTestData\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Timesheet/Calculator/RateCalculatorTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Timesheet\\\\DateTimeFactoryTest\\:\\:getEndOfWeekData\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -2065,111 +1891,11 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Timesheet/LockdownServiceTest.php
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getRateTestData\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Timesheet/RateServiceTest.php
|
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\\.$#"
|
message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#"
|
||||||
count: 6
|
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\\.$#"
|
message: "#^Method App\\\\Tests\\\\Widget\\\\Type\\\\UserDurationYearTest\\:\\:getDefaultOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: Widget/Type/UserDurationYearTest.php
|
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>
|
<source>copy_teams_on_create</source>
|
||||||
<target>Übernehme Teams beim Erstellen neuer Einträge vom angemeldeten Benutzer</target>
|
<target>Übernehme Teams beim Erstellen neuer Einträge vom angemeldeten Benutzer</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="6AaVxB2" resname="invoice.rounding_mode" approved="yes">
|
||||||
|
<source>invoice.rounding_mode</source>
|
||||||
|
<target>Rundungsmodus</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -310,6 +310,10 @@
|
|||||||
<source>copy_teams_on_create</source>
|
<source>copy_teams_on_create</source>
|
||||||
<target>Take over teams from the logged-in user when creating new entries</target>
|
<target>Take over teams from the logged-in user when creating new entries</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="6AaVxB2" resname="invoice.rounding_mode" approved="yes">
|
||||||
|
<source>invoice.rounding_mode</source>
|
||||||
|
<target>Rounding mode</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user