diff --git a/config/services.yaml b/config/services.yaml index be0b14f0..57b64d43 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -86,6 +86,9 @@ services: App\Timesheet\RateService: arguments: ['%kimai.timesheet.rates%'] + App\Timesheet\RateCalculator\RateCalculatorMode: + factory: ['@App\Timesheet\RateCalculator\RateCalculatorFactory', getRateCalculatorMode] + # ================================================================================ # SECURITY & VOTER # ================================================================================ diff --git a/phpstan.neon b/phpstan.neon index f8238a78..670f2c0d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -969,11 +969,6 @@ parameters: count: 1 path: src/DataFixtures/TimesheetFixtures.php - - - message: "#^Parameter \\#1 \\$user of method App\\\\DataFixtures\\\\TimesheetFixtures\\:\\:createTimesheetEntry\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#" - count: 2 - path: src/DataFixtures/TimesheetFixtures.php - - message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:createPermissionParameter\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" count: 1 diff --git a/src/Controller/SystemConfigurationController.php b/src/Controller/SystemConfigurationController.php index 181a757a..9355a274 100644 --- a/src/Controller/SystemConfigurationController.php +++ b/src/Controller/SystemConfigurationController.php @@ -449,6 +449,11 @@ final class SystemConfigurationController extends AbstractController ->setType(TextType::class) ->setConstraints([new NotBlank()]) ->setTranslationDomain('system-configuration'), + (new Configuration('invoice.rounding_mode')) + ->setLabel('invoice.rounding_mode') + ->setType(ChoiceType::class) + ->setOptions(['choices' => ['classic' => 'classic', 'decimal' => 'decimal']]) + ->setTranslationDomain('system-configuration'), ]), $authentication, (new SystemConfigurationModel('customer')) diff --git a/src/DataFixtures/TimesheetFixtures.php b/src/DataFixtures/TimesheetFixtures.php index b047674a..b04f2bd5 100644 --- a/src/DataFixtures/TimesheetFixtures.php +++ b/src/DataFixtures/TimesheetFixtures.php @@ -15,7 +15,8 @@ use App\Entity\Tag; use App\Entity\Timesheet; use App\Entity\User; use App\Entity\UserPreference; -use App\Timesheet\Util; +use App\Timesheet\RateCalculator\ClassicRateCalculator; +use App\Timesheet\RateCalculator\RateCalculatorMode; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface; use Doctrine\Persistence\ObjectManager; @@ -60,10 +61,14 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface $allUser = $this->getAllUsers($manager); $faker = Factory::create(); $all = 0; + $calculator = new ClassicRateCalculator(); foreach ($allUser as $user) { // reload, because the manager might have been cleared $user = $manager->find(User::class, $user->getId()); + if ($user === null) { + continue; + } // random amount of timesheet entries for every user $timesheetForUser = rand(self::MIN_TIMESHEETS_PER_USER, self::MAX_TIMESHEETS_PER_USER); @@ -84,6 +89,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface } $entry = $this->createTimesheetEntry( + $calculator, $user, $activities[array_rand($activities)], $projects[array_rand($projects)], @@ -98,6 +104,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface // create active records if ($all % 3 === 0) { $entry = $this->createTimesheetEntry( + $calculator, $user, $activities[array_rand($activities)], $projects[array_rand($projects)], @@ -208,7 +215,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface return $this->findRandom($manager, Activity::class, 50); } - private function createTimesheetEntry(User $user, Activity $activity, Project $project, ?string $description, bool $setEndDate): Timesheet + private function createTimesheetEntry(RateCalculatorMode $calculatorMode, User $user, Activity $activity, Project $project, ?string $description, bool $setEndDate): Timesheet { $start = $this->getRandomFirstDay(); $start = $start->modify('- ' . (rand(1, 86400)) . ' seconds'); @@ -227,7 +234,7 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface $duration = $end->getTimestamp() - $start->getTimestamp(); $hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE); - $rate = Util::calculateRate($hourlyRate, $duration); + $rate = $calculatorMode->calculateRate($hourlyRate, $duration); $entry->setEnd($end); $entry->setRate($rate); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 35d55250..0dd86ac1 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -369,6 +369,10 @@ final class Configuration implements ConfigurationInterface ->booleanNode('upload_twig') ->defaultFalse() ->end() + ->enumNode('rounding_mode') + ->values(['decimal', 'classic']) + ->defaultValue('classic') + ->end() ->end() ; diff --git a/src/Invoice/Calculator/AbstractCalculator.php b/src/Invoice/Calculator/AbstractCalculator.php index c46ea3ab..aa09853f 100644 --- a/src/Invoice/Calculator/AbstractCalculator.php +++ b/src/Invoice/Calculator/AbstractCalculator.php @@ -12,7 +12,6 @@ namespace App\Invoice\Calculator; use App\Invoice\InvoiceItem; use App\Invoice\InvoiceModel; use App\Invoice\TaxRow; -use App\Timesheet\Util; abstract class AbstractCalculator { @@ -42,10 +41,10 @@ abstract class AbstractCalculator if (\count($this->cached) === 0) { foreach ($this->calculateEntries() as $entry) { if (!$entry->isFixedRate() && $entry->getHourlyRate() !== null && $entry->getHourlyRate() > 0) { - $entry->setDuration(Util::decimalizeDuration($entry->getDuration())); + $entry->setDuration($this->model->getRateCalculatorMode()->roundDuration($entry->getDuration())); // when merging many entries, we might run into rounding issues // so we have to recalculate the hourly rate here - $entry->setRate(Util::calculateRate($entry->getHourlyRate(), $entry->getDuration())); + $entry->setRate($this->model->getRateCalculatorMode()->calculateRate($entry->getHourlyRate(), $entry->getDuration())); } $this->cached[] = $entry; diff --git a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php index 00ab8346..4e5b4f31 100644 --- a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php @@ -32,7 +32,7 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator $values = [ $prefix . 'id' => $customer->getId(), - $prefix . 'address' => $customer->getFormattedAddress() ?? '', + $prefix . 'address' => $customer->getFormattedAddress() ?? '', // deprecated since 2.44 $prefix . 'address_line1' => $customer->getAddressLine1() ?? '', $prefix . 'address_line2' => $customer->getAddressLine2() ?? '', $prefix . 'address_line3' => $customer->getAddressLine3() ?? '', diff --git a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php index 4dd67ffe..1c90e054 100644 --- a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php @@ -62,12 +62,12 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'invoice.subtotal_plain' => $subtotal, 'template.name' => $template->getName() ?? '', - 'template.company' => $template->getCompany() ?? '', + 'template.company' => $template->getCompany() ?? '', // deprecated since 2.45 'template.address' => $template->getAddress() ?? '', 'template.title' => $template->getTitle() ?? '', 'template.payment_terms' => $template->getPaymentTerms() ?? '', 'template.due_days' => $template->getDueDays(), - 'template.vat_id' => $template->getVatId() ?? '', + 'template.vat_id' => $template->getVatId() ?? '', // deprecated since 2.45 'template.contact' => $template->getContact() ?? '', 'template.country' => null, 'template.country_name' => null, @@ -75,16 +75,16 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'query.begin' => '', 'query.begin_day' => '', - 'query.begin_process' => null, // since 2.14 + 'query.begin_process' => null, // since 2.14 'query.begin_month' => '', 'query.begin_month_number' => '', 'query.begin_year' => '', - 'query.end' => '', // since 1.9 - 'query.end_day' => '', // since 1.9 - 'query.end_process' => null, // since 2.14 - 'query.end_month' => '', // since 1.9 - 'query.end_month_number' => '', // since 1.9 - 'query.end_year' => '', // since 1.9 + 'query.end' => '', // since 1.9 + 'query.end_day' => '', // since 1.9 + 'query.end_process' => null, // since 2.14 + 'query.end_month' => '', // since 1.9 + 'query.end_month_number' => '', // since 1.9 + 'query.end_year' => '', // since 1.9 // since 2.0.15 'user.see_others' => ($model->getQuery()?->getUser() === null), @@ -105,8 +105,8 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator if ($seller !== null) { $country = $seller->getCountry(); if ($country !== null) { - $values['template.country'] = $country; - $values['template.country_name'] = Countries::getName($country, $language); + $values['template.country'] = $country; // deprecated since 2.45 + $values['template.country_name'] = Countries::getName($country, $language); // deprecated since 2.45 } } @@ -116,14 +116,10 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator if ($begin !== null) { $values = array_merge($values, [ 'query.day' => $begin->format('d'), - // @deprecated - but impossible to delete - 'query.month' => $formatter->getFormattedMonthName($begin), - // @deprecated - but impossible to delete - 'query.month_number' => $begin->format('m'), - // @deprecated - but impossible to delete - 'query.year' => $begin->format('Y'), - // @deprecated - but impossible to delete - 'query.begin' => $formatter->getFormattedDateTime($begin), + 'query.month' => $formatter->getFormattedMonthName($begin), // @deprecated - but impossible to delete + 'query.month_number' => $begin->format('m'), // @deprecated - but impossible to delete + 'query.year' => $begin->format('Y'), // @deprecated - but impossible to delete + 'query.begin' => $formatter->getFormattedDateTime($begin), // @deprecated - but impossible to delete 'query.begin_process' => $begin->format(self::DATE_PROCESS_FORMAT), // since 2.14 'query.begin_day' => $begin->format('d'), 'query.begin_month' => $formatter->getFormattedMonthName($begin), diff --git a/src/Invoice/Hydrator/InvoiceModelIssuerHydrator.php b/src/Invoice/Hydrator/InvoiceModelIssuerHydrator.php new file mode 100644 index 00000000..712d0ddf --- /dev/null +++ b/src/Invoice/Hydrator/InvoiceModelIssuerHydrator.php @@ -0,0 +1,65 @@ + + */ + 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; + } +} diff --git a/src/Invoice/InvoiceModel.php b/src/Invoice/InvoiceModel.php index 3858daa0..f7ec99c6 100644 --- a/src/Invoice/InvoiceModel.php +++ b/src/Invoice/InvoiceModel.php @@ -19,10 +19,12 @@ use App\Invoice\Hydrator\InvoiceItemDefaultHydrator; use App\Invoice\Hydrator\InvoiceModelActivityHydrator; use App\Invoice\Hydrator\InvoiceModelCustomerHydrator; use App\Invoice\Hydrator\InvoiceModelDefaultHydrator; +use App\Invoice\Hydrator\InvoiceModelIssuerHydrator; use App\Invoice\Hydrator\InvoiceModelProjectHydrator; use App\Invoice\Hydrator\InvoiceModelUserHydrator; use App\Project\ProjectStatisticService; use App\Repository\Query\InvoiceQuery; +use App\Timesheet\RateCalculator\RateCalculatorMode; use Symfony\Component\DependencyInjection\Attribute\Exclude; /** @@ -68,12 +70,14 @@ final class InvoiceModel ActivityStatisticService $activityStatistic, private readonly Customer $customer, private readonly InvoiceTemplate $template, + private readonly RateCalculatorMode $rateCalculatorMode ) { $this->invoiceDate = new \DateTimeImmutable(); $this->formatter = $formatter; $this->addModelHydrator(new InvoiceModelDefaultHydrator()); $this->addModelHydrator(new InvoiceModelCustomerHydrator($customerStatistic)); + $this->addModelHydrator(new InvoiceModelIssuerHydrator()); $this->addModelHydrator(new InvoiceModelProjectHydrator($projectStatistic)); $this->addModelHydrator(new InvoiceModelActivityHydrator($activityStatistic)); $this->addModelHydrator(new InvoiceModelUserHydrator()); @@ -207,6 +211,11 @@ final class InvoiceModel return $this->calculator; } + public function getRateCalculatorMode(): RateCalculatorMode + { + return $this->rateCalculatorMode; + } + /** * Returns the user currently creating the invoice. */ diff --git a/src/Invoice/InvoiceModelFactory.php b/src/Invoice/InvoiceModelFactory.php index c467c663..60e6d87d 100644 --- a/src/Invoice/InvoiceModelFactory.php +++ b/src/Invoice/InvoiceModelFactory.php @@ -15,19 +15,21 @@ use App\Entity\Customer; use App\Entity\InvoiceTemplate; use App\Project\ProjectStatisticService; use App\Repository\Query\InvoiceQuery; +use App\Timesheet\RateCalculator\RateCalculatorMode; final class InvoiceModelFactory { public function __construct( private readonly CustomerStatisticService $customerStatisticService, private readonly ProjectStatisticService $projectStatisticService, - private readonly ActivityStatisticService $activityStatisticService + private readonly ActivityStatisticService $activityStatisticService, + private readonly RateCalculatorMode $rateCalculatorMode ) { } public function createModel(InvoiceFormatter $formatter, Customer $customer, InvoiceTemplate $template, InvoiceQuery $query): InvoiceModel { - $model = new InvoiceModel($formatter, $this->customerStatisticService, $this->projectStatisticService, $this->activityStatisticService, $customer, $template); + $model = new InvoiceModel($formatter, $this->customerStatisticService, $this->projectStatisticService, $this->activityStatisticService, $customer, $template, $this->rateCalculatorMode); $model->setQuery($query); diff --git a/src/Invoice/ServiceInvoice.php b/src/Invoice/ServiceInvoice.php index 0e4d90d3..01a3dbf5 100644 --- a/src/Invoice/ServiceInvoice.php +++ b/src/Invoice/ServiceInvoice.php @@ -49,11 +49,11 @@ final class ServiceInvoice private array $invoiceItemRepositories = []; public function __construct( - private InvoiceDocumentRepository $documents, - private FileHelper $fileHelper, - private InvoiceRepository $invoiceRepository, - private LocaleService $formatter, - private InvoiceModelFactory $invoiceModelFactory + private readonly InvoiceDocumentRepository $documents, + private readonly FileHelper $fileHelper, + private readonly InvoiceRepository $invoiceRepository, + private readonly LocaleService $formatter, + private readonly InvoiceModelFactory $invoiceModelFactory ) { } diff --git a/src/Timesheet/Calculator/RateCalculator.php b/src/Timesheet/Calculator/RateCalculator.php index 1b4e2817..ba0ee330 100644 --- a/src/Timesheet/Calculator/RateCalculator.php +++ b/src/Timesheet/Calculator/RateCalculator.php @@ -18,7 +18,7 @@ use App\Timesheet\RateServiceInterface; */ final class RateCalculator implements CalculatorInterface { - public function __construct(private RateServiceInterface $service) + public function __construct(private readonly RateServiceInterface $service) { } diff --git a/src/Timesheet/RateCalculator/ClassicRateCalculator.php b/src/Timesheet/RateCalculator/ClassicRateCalculator.php new file mode 100644 index 00000000..e1108f1b --- /dev/null +++ b/src/Timesheet/RateCalculator/ClassicRateCalculator.php @@ -0,0 +1,31 @@ +configuration->find('invoice.rounding_mode') === 'decimal') { + return new DecimalRateCalculator(); + } + + return new ClassicRateCalculator(); + } +} diff --git a/src/Timesheet/RateCalculator/RateCalculatorMode.php b/src/Timesheet/RateCalculator/RateCalculatorMode.php new file mode 100644 index 00000000..e15bb0d1 --- /dev/null +++ b/src/Timesheet/RateCalculator/RateCalculatorMode.php @@ -0,0 +1,23 @@ +getDuration()) { - $totalRate = Util::calculateRate($factoredHourlyRate, $record->getDuration()); - $totalInternalRate = Util::calculateRate($factoredInternalRate, $record->getDuration()); + $totalRate = $this->calculatorMode->calculateRate($factoredHourlyRate, $record->getDuration()); + $totalInternalRate = $this->calculatorMode->calculateRate($factoredInternalRate, $record->getDuration()); } return new Rate($totalRate, $totalInternalRate, $factoredHourlyRate, null); diff --git a/src/Timesheet/Util.php b/src/Timesheet/Util.php index d9e6e786..d8564cb6 100644 --- a/src/Timesheet/Util.php +++ b/src/Timesheet/Util.php @@ -10,7 +10,7 @@ namespace App\Timesheet; /** - * A static helper class for re-usable functionality. + * @deprecated use RateCalculatorMode instead */ final class Util { @@ -29,7 +29,7 @@ final class Util } /** - * Makes sure tha the duration is full compatible with decimal format, stripping away overflowing seconds. + * Makes sure that the duration is full compatible with decimal format, stripping away overflowing seconds. */ public static function decimalizeDuration(int $seconds): int { diff --git a/tests/API/RateControllerTestTrait.php b/tests/API/RateControllerTestTrait.php index 319d0e4b..932ab287 100644 --- a/tests/API/RateControllerTestTrait.php +++ b/tests/API/RateControllerTestTrait.php @@ -15,9 +15,6 @@ use App\Entity\ProjectRate; use App\Entity\RateInterface; use App\Entity\User; -/** - * @group integration - */ trait RateControllerTestTrait { abstract protected function getRateUrl(?int $id = 1, ?int $rateId = null): string; diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php index dab23688..d81eb3fb 100644 --- a/tests/API/TimesheetControllerTest.php +++ b/tests/API/TimesheetControllerTest.php @@ -426,8 +426,8 @@ class TimesheetControllerTest extends APIControllerBaseTestCase 'exported' => true, 'metaFields' => [], 'hourlyRate' => 137.21, - 'rate' => 1772.75, // 12,92 * 137,21 - 'internalRate' => 1772.75, + 'rate' => 1772.2958, + 'internalRate' => 1772.2958, ]; foreach ($expected as $key => $value) { diff --git a/tests/Controller/TimesheetTeamControllerTest.php b/tests/Controller/TimesheetTeamControllerTest.php index 5853f1b9..2a14d65d 100644 --- a/tests/Controller/TimesheetTeamControllerTest.php +++ b/tests/Controller/TimesheetTeamControllerTest.php @@ -16,7 +16,6 @@ use App\Form\Type\TagsType; use App\Tests\DataFixtures\TagFixtures; use App\Tests\DataFixtures\TimesheetFixtures; use App\Timesheet\DateTimeFactory; -use App\Timesheet\Util; use PHPUnit\Framework\Attributes\Group; #[Group('integration')] @@ -414,7 +413,7 @@ class TimesheetTeamControllerTest extends AbstractControllerBaseTestCase self::assertCount(3, $timesheet->getTags()); self::assertEquals($newUser->getId(), $timesheet->getUser()->getId()); self::assertTrue($timesheet->isExported()); - self::assertEquals(Util::calculateRate(13.78, $timesheet->getDuration()), $timesheet->getRate()); + self::assertGreaterThan(0, $timesheet->getRate()); } } diff --git a/tests/DataFixtures/TimesheetFixtures.php b/tests/DataFixtures/TimesheetFixtures.php index 9e6da43c..d45e97df 100644 --- a/tests/DataFixtures/TimesheetFixtures.php +++ b/tests/DataFixtures/TimesheetFixtures.php @@ -15,7 +15,8 @@ use App\Entity\Tag; use App\Entity\Timesheet; use App\Entity\User; use App\Entity\UserPreference; -use App\Timesheet\Util; +use App\Timesheet\RateCalculator\ClassicRateCalculator; +use App\Timesheet\RateCalculator\RateCalculatorMode; use Doctrine\Persistence\ObjectManager; use Faker\Factory; @@ -205,6 +206,7 @@ final class TimesheetFixtures implements TestFixture } } $manager->flush(); + $calculator = new ClassicRateCalculator(); for ($i = 0; $i < $this->amount; $i++) { $description = $faker->text(); @@ -217,6 +219,9 @@ final class TimesheetFixtures implements TestFixture } $user = $users[array_rand($users)]; + if ($user === null) { + continue; + } $activity = $activities[array_rand($activities)]; $project = $activity->getProject(); @@ -225,6 +230,7 @@ final class TimesheetFixtures implements TestFixture } $timesheet = $this->createTimesheetEntry( + $calculator, $user, $activity, $project, @@ -244,12 +250,16 @@ final class TimesheetFixtures implements TestFixture $activity = $activities[array_rand($activities)]; $project = $activity->getProject(); $user = $users[array_rand($users)]; + if ($user === null) { + continue; + } - if (null === $project) { + if ($project === null) { $project = $projects[array_rand($projects)]; } $timesheet = $this->createTimesheetEntry( + $calculator, $user, $activity, $project, @@ -310,17 +320,25 @@ final class TimesheetFixtures implements TestFixture } /** - * @param \DateTime $start * @param array $tagArray */ - private function createTimesheetEntry(User $user, Activity $activity, Project $project, ?string $description, \DateTime $start, array $tagArray = [], bool $setEndDate = true): Timesheet + private function createTimesheetEntry( + RateCalculatorMode $calculatorMode, + User $user, + Activity $activity, + Project $project, + ?string $description, + \DateTime $start, + array $tagArray = [], + bool $setEndDate = true + ): Timesheet { $end = clone $start; $end = $end->modify('+ ' . (rand(1, 86400)) . ' seconds'); $duration = $end->getTimestamp() - $start->getTimestamp(); $hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE); - $rate = Util::calculateRate($hourlyRate, $duration); + $rate = $calculatorMode->calculateRate($hourlyRate, $duration); $entry = new Timesheet(); $entry->setActivity($activity); diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index f5fd6835..1bd2bf01 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -310,6 +310,7 @@ class ConfigurationTest extends TestCase ], 'number_format' => '{Y}/{cy,3}', 'upload_twig' => false, + 'rounding_mode' => 'classic', ], 'export' => [ 'documents' => [ diff --git a/tests/Export/Base/HtmlRendererTest.php b/tests/Export/Base/HtmlRendererTest.php index 3a59e5f4..0895a7eb 100644 --- a/tests/Export/Base/HtmlRendererTest.php +++ b/tests/Export/Base/HtmlRendererTest.php @@ -48,9 +48,7 @@ class HtmlRendererTest extends AbstractRendererTestCase self::assertFalse($sut->isInternal()); } - /** - * @group legacy - */ + #[Group('legacy')] public function testLegacy(): void { $sut = $this->getAbstractRenderer(); diff --git a/tests/Export/Base/PdfRendererTest.php b/tests/Export/Base/PdfRendererTest.php index 24fe740e..74c4532c 100644 --- a/tests/Export/Base/PdfRendererTest.php +++ b/tests/Export/Base/PdfRendererTest.php @@ -58,9 +58,7 @@ class PdfRendererTest extends AbstractRendererTestCase self::assertFalse($sut->isInternal()); } - /** - * @group legacy - */ + #[Group('legacy')] public function testLegacy(): void { $sut = $this->getAbstractRenderer(); diff --git a/tests/Invoice/Hydrator/InvoiceModelIssuerHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelIssuerHydratorTest.php new file mode 100644 index 00000000..adc91837 --- /dev/null +++ b/tests/Invoice/Hydrator/InvoiceModelIssuerHydratorTest.php @@ -0,0 +1,96 @@ +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); + } +} diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php index e40fc38e..484a5336 100644 --- a/tests/Invoice/Renderer/DebugRendererTest.php +++ b/tests/Invoice/Renderer/DebugRendererTest.php @@ -109,6 +109,29 @@ class DebugRendererTest extends TestCase 'invoice.subtotal', 'invoice.subtotal_nc', 'invoice.subtotal_plain', + 'issuer.address', + 'issuer.address_line1', + 'issuer.address_line2', + 'issuer.address_line3', + 'issuer.buyer_reference', + 'issuer.city', + 'issuer.comment', + 'issuer.company', + 'issuer.contact', + 'issuer.country', + 'issuer.country_name', + 'issuer.email', + 'issuer.fax', + 'issuer.homepage', + 'issuer.id', + 'issuer.invoice_text', + 'issuer.meta.foo-customer', + 'issuer.mobile', + 'issuer.name', + 'issuer.number', + 'issuer.phone', + 'issuer.postcode', + 'issuer.vat_id', 'template.name', 'template.company', 'template.country', diff --git a/tests/Invoice/Renderer/RendererTestTrait.php b/tests/Invoice/Renderer/RendererTestTrait.php index fb8f79e9..5180d601 100644 --- a/tests/Invoice/Renderer/RendererTestTrait.php +++ b/tests/Invoice/Renderer/RendererTestTrait.php @@ -54,9 +54,17 @@ trait RendererTestTrait ); } + /** + * @param class-string $classname + */ protected function getAbstractRenderer(string $classname): AbstractRenderer { - return new $classname(); + $t = new $classname(); + if (!$t instanceof AbstractRenderer) { + throw new \InvalidArgumentException('Not an instance of AbstractRenderer: ' . \get_class($t)); + } + + return $t; } protected function getFormatter(): InvoiceFormatter diff --git a/tests/Mocks/AbstractMockFactory.php b/tests/Mocks/AbstractMockFactory.php index 3a108418..ae44b3cf 100644 --- a/tests/Mocks/AbstractMockFactory.php +++ b/tests/Mocks/AbstractMockFactory.php @@ -10,11 +10,12 @@ namespace App\Tests\Mocks; use PHPUnit\Framework\MockObject\MockBuilder; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; abstract class AbstractMockFactory { - public function __construct(private TestCase $testCase) + public function __construct(private readonly TestCase $testCase) { } @@ -23,11 +24,19 @@ abstract class AbstractMockFactory return $this->testCase; } - protected function createMock(string $className) + /** + * @template T of object + * @param class-string $className + * @return T&MockObject + */ + protected function createMock(string $className): object { - return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock(); + return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock(); // @phpstan-ignore return.type } + /** + * @param class-string $className + */ protected function getMockBuilder(string $className): MockBuilder { return new MockBuilder($this->testCase, $className); diff --git a/tests/Mocks/InvoiceModelFactoryFactory.php b/tests/Mocks/InvoiceModelFactoryFactory.php index ffb4b322..7eaab381 100644 --- a/tests/Mocks/InvoiceModelFactoryFactory.php +++ b/tests/Mocks/InvoiceModelFactoryFactory.php @@ -13,6 +13,7 @@ use App\Activity\ActivityStatisticService; use App\Customer\CustomerStatisticService; use App\Invoice\InvoiceModelFactory; use App\Project\ProjectStatisticService; +use App\Timesheet\RateCalculator\DecimalRateCalculator; class InvoiceModelFactoryFactory extends AbstractMockFactory { @@ -24,7 +25,8 @@ class InvoiceModelFactoryFactory extends AbstractMockFactory $projectStatistic = $this->getMockBuilder(ProjectStatisticService::class)->disableOriginalConstructor()->getMock(); /** @var ActivityStatisticService $activityStatistic */ $activityStatistic = $this->getMockBuilder(ActivityStatisticService::class)->disableOriginalConstructor()->getMock(); + $rateMode = new DecimalRateCalculator(); - return new InvoiceModelFactory($customerStatistic, $projectStatistic, $activityStatistic); + return new InvoiceModelFactory($customerStatistic, $projectStatistic, $activityStatistic, $rateMode); } } diff --git a/tests/Mocks/RateServiceFactory.php b/tests/Mocks/RateServiceFactory.php new file mode 100644 index 00000000..1a26397e --- /dev/null +++ b/tests/Mocks/RateServiceFactory.php @@ -0,0 +1,27 @@ +createMock(TimesheetRepository::class); + if (!empty($rates)) { + $mock->expects($this->getTestCase()->any())->method('findMatchingRates')->willReturn($rates); + } + + return new RateService($rules, $mock, new ClassicRateCalculator()); + } +} diff --git a/tests/Mocks/SystemConfigurationFactory.php b/tests/Mocks/SystemConfigurationFactory.php index a5e26beb..0975e71e 100644 --- a/tests/Mocks/SystemConfigurationFactory.php +++ b/tests/Mocks/SystemConfigurationFactory.php @@ -17,7 +17,6 @@ class SystemConfigurationFactory { /** * @param array $settings - * @return SystemConfiguration */ public static function create(ConfigLoaderInterface $repository, array $settings): SystemConfiguration { @@ -26,7 +25,6 @@ class SystemConfigurationFactory /** * @param array $settings - * @return SystemConfiguration */ public static function createStub(array $settings = []): SystemConfiguration { diff --git a/tests/Timesheet/Calculator/RateCalculatorTest.php b/tests/Timesheet/Calculator/RateCalculatorTest.php index eac1a24f..9ec879bb 100644 --- a/tests/Timesheet/Calculator/RateCalculatorTest.php +++ b/tests/Timesheet/Calculator/RateCalculatorTest.php @@ -18,7 +18,7 @@ use App\Entity\ProjectRate; use App\Entity\Timesheet; use App\Entity\User; use App\Entity\UserPreference; -use App\Repository\TimesheetRepository; +use App\Tests\Mocks\RateServiceFactory; use App\Timesheet\Calculator\RateCalculator; use App\Timesheet\RateService; use PHPUnit\Framework\Attributes\CoversClass; @@ -28,14 +28,11 @@ use PHPUnit\Framework\TestCase; #[CoversClass(RateCalculator::class)] class RateCalculatorTest extends TestCase { - protected function getRateRepositoryMock(array $rates = []): TimesheetRepository + private function getRateService(array $rules = [], array $rates = []): RateService { - $mock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock(); - if (!empty($rates)) { - $mock->expects($this->any())->method('findMatchingRates')->willReturn($rates); - } + $factory = new RateServiceFactory($this); - return $mock; + return $factory->create($rules, $rates); } private function assertRateByTimesheetHourlyRate(int $duration, float $hourlyRate, float $rate): void @@ -47,7 +44,7 @@ class RateCalculatorTest extends TestCase $record->setActivity(new Activity()); $record->setUser($this->getTestUser()); - $sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock())); + $sut = new RateCalculator($this->getRateService()); $sut->calculate($record, []); self::assertEquals($rate, $record->getRate()); } @@ -55,9 +52,9 @@ class RateCalculatorTest extends TestCase public function testCalculateWithTimesheetHourlyRate(): void { $this->assertRateByTimesheetHourlyRate(1800, 100, 50); - $this->assertRateByTimesheetHourlyRate(400, 100, 11); - $this->assertRateByTimesheetHourlyRate(1234, 100, 34); - $this->assertRateByTimesheetHourlyRate(2739, 100, 76); + $this->assertRateByTimesheetHourlyRate(400, 100, 11.1111); + $this->assertRateByTimesheetHourlyRate(1234, 100, 34.2778); + $this->assertRateByTimesheetHourlyRate(2739, 100, 76.0833); } public function testCalculateWithTimesheetFixedRate(): void @@ -71,7 +68,7 @@ class RateCalculatorTest extends TestCase $record->setActivity(new Activity()); $record->setUser($this->getTestUser()); - $sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock())); + $sut = new RateCalculator($this->getRateService()); $sut->calculate($record, []); self::assertEquals(10, $record->getRate()); } @@ -112,22 +109,22 @@ class RateCalculatorTest extends TestCase #[DataProvider('getRateTestData')] public function testRates( - $expectedRate, - $expectedInternalRate, - $duration, - $userRate, - $userInternalRate, - $timesheetHourly, - $timesheetFixed, - $activityRate, - $activityInternal, - $activityIsFixed, - $projectRate, - $projectInternal, - $projectIsFixed, - $customerRate, - $customerInternal, - $customerIsFixed + float $expectedRate, + float $expectedInternalRate, + int $duration, + float $userRate, + ?float $userInternalRate, + ?float $timesheetHourly, + ?float $timesheetFixed, + ?float $activityRate, + ?float $activityInternal, + bool $activityIsFixed, + ?float $projectRate, + ?float $projectInternal, + bool $projectIsFixed, + ?float $customerRate, + ?float $customerInternal, + bool $customerIsFixed ) { $customer = new Customer('foo'); @@ -178,7 +175,7 @@ class RateCalculatorTest extends TestCase $rates[] = $rate; } - $sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock($rates))); + $sut = new RateCalculator($this->getRateService([], $rates)); $sut->calculate($timesheet, []); self::assertEquals($expectedRate, $timesheet->getRate()); self::assertEquals($expectedInternalRate, $timesheet->getInternalRate()); @@ -207,7 +204,7 @@ class RateCalculatorTest extends TestCase self::assertEquals(0, $record->getRate()); - $sut = new RateCalculator(new RateService([], $this->getRateRepositoryMock())); + $sut = new RateCalculator($this->getRateService()); $sut->calculate($record, []); self::assertEquals(0, $record->getRate()); } @@ -216,7 +213,7 @@ class RateCalculatorTest extends TestCase * Uses the hourly rate from user_preferences to calculate the rate. */ #[DataProvider('getRuleDefinitions')] - public function testCalculateWithRulesByUsersHourlyRate($duration, $rules, $expectedRate): void + public function testCalculateWithRulesByUsersHourlyRate(int $duration, array $rules, float $expectedRate): void { $end = new \DateTime('12:00:00', new \DateTimeZone('UTC')); $start = clone $end; @@ -232,7 +229,7 @@ class RateCalculatorTest extends TestCase $record->setEnd($end); - $sut = new RateCalculator(new RateService($rules, $this->getRateRepositoryMock())); + $sut = new RateCalculator($this->getRateService($rules)); $sut->calculate($record, []); self::assertEquals($expectedRate, $record->getRate()); @@ -247,7 +244,7 @@ class RateCalculatorTest extends TestCase [ 31837, // 31824 = 8,84 [], - 663 + 663.2708 ], [ 31837, // 31824 = 8,84 @@ -261,7 +258,7 @@ class RateCalculatorTest extends TestCase 'factor' => 1.5 ], ], - 1326 // 8,84 * 75 (see user) * 2 + 1326.5417 ], [ 31837, // 31824 = 8,84 @@ -275,7 +272,7 @@ class RateCalculatorTest extends TestCase 'factor' => 1.5 ], ], - 2320.5 // 75 * 8,84 * 3,5 + 2321.4479 ], ]; } diff --git a/tests/Timesheet/RateCalculator/ClassicRateCalculatorTest.php b/tests/Timesheet/RateCalculator/ClassicRateCalculatorTest.php new file mode 100644 index 00000000..6ef004be --- /dev/null +++ b/tests/Timesheet/RateCalculator/ClassicRateCalculatorTest.php @@ -0,0 +1,130 @@ +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>|\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)); + } + } +} diff --git a/tests/Timesheet/RateCalculator/DecimalRateCalculatorTest.php b/tests/Timesheet/RateCalculator/DecimalRateCalculatorTest.php new file mode 100644 index 00000000..a1b55365 --- /dev/null +++ b/tests/Timesheet/RateCalculator/DecimalRateCalculatorTest.php @@ -0,0 +1,142 @@ +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>|\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])); + } + } +} diff --git a/tests/Timesheet/RateCalculator/RateCalculatorFactoryTest.php b/tests/Timesheet/RateCalculator/RateCalculatorFactoryTest.php new file mode 100644 index 00000000..3ff86fc6 --- /dev/null +++ b/tests/Timesheet/RateCalculator/RateCalculatorFactoryTest.php @@ -0,0 +1,48 @@ +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); + } +} diff --git a/tests/Timesheet/RateServiceTest.php b/tests/Timesheet/RateServiceTest.php index d26b399a..57022675 100644 --- a/tests/Timesheet/RateServiceTest.php +++ b/tests/Timesheet/RateServiceTest.php @@ -18,7 +18,7 @@ use App\Entity\ProjectRate; use App\Entity\Timesheet; use App\Entity\User; use App\Entity\UserPreference; -use App\Repository\TimesheetRepository; +use App\Tests\Mocks\RateServiceFactory; use App\Timesheet\RateService; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; @@ -27,14 +27,11 @@ use PHPUnit\Framework\TestCase; #[CoversClass(RateService::class)] class RateServiceTest extends TestCase { - protected function getRateRepositoryMock(array $rates = []): TimesheetRepository + private function getSut(array $rules = [], array $rates = []): RateService { - $mock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock(); - if (!empty($rates)) { - $mock->expects($this->any())->method('findMatchingRates')->willReturn($rates); - } + $factory = new RateServiceFactory($this); - return $mock; + return $factory->create($rules, $rates); } private static function createDateTime(?string $datetime = null): \DateTime @@ -51,7 +48,7 @@ class RateServiceTest extends TestCase $record->setActivity(new Activity()); $record->setUser($this->getTestUser()); - $sut = new RateService([], $this->getRateRepositoryMock()); + $sut = $this->getSut(); $rate = $sut->calculate($record); self::assertEquals(50, $rate->getRate()); } @@ -67,7 +64,7 @@ class RateServiceTest extends TestCase $record->setActivity(new Activity()); $record->setUser($this->getTestUser()); - $sut = new RateService([], $this->getRateRepositoryMock()); + $sut = $this->getSut(); $rate = $sut->calculate($record); self::assertEquals(10, $rate->getRate()); } @@ -108,22 +105,22 @@ class RateServiceTest extends TestCase #[DataProvider('getRateTestData')] public function testRates( - $expectedRate, - $expectedInternalRate, - $duration, - $userRate, - $userInternalRate, - $timesheetHourly, - $timesheetFixed, - $activityRate, - $activityInternal, - $activityIsFixed, - $projectRate, - $projectInternal, - $projectIsFixed, - $customerRate, - $customerInternal, - $customerIsFixed + float $expectedRate, + float $expectedInternalRate, + int $duration, + float $userRate, + ?float $userInternalRate, + ?float $timesheetHourly, + ?float $timesheetFixed, + ?float $activityRate, + ?float $activityInternal, + bool $activityIsFixed, + ?float $projectRate, + ?float $projectInternal, + bool $projectIsFixed, + ?float $customerRate, + ?float $customerInternal, + bool $customerIsFixed ): void { $customer = new Customer('foo'); @@ -174,7 +171,7 @@ class RateServiceTest extends TestCase $rates[] = $rate; } - $sut = new RateService([], $this->getRateRepositoryMock($rates)); + $sut = $this->getSut([], $rates); $rate = $sut->calculate($timesheet); self::assertEquals($expectedRate, $rate->getRate()); self::assertEquals($expectedInternalRate, $rate->getInternalRate()); @@ -203,7 +200,7 @@ class RateServiceTest extends TestCase self::assertEquals(0, $record->getRate()); - $sut = new RateService([], $this->getRateRepositoryMock()); + $sut = $this->getSut(); $rate = $sut->calculate($record); self::assertEquals(0, $rate->getRate()); } @@ -212,7 +209,7 @@ class RateServiceTest extends TestCase * Uses the hourly rate from user_preferences to calculate the rate. */ #[DataProvider('getRuleDefinitions')] - public function testCalculateWithRulesByUsersHourlyRate($duration, $rules, $expectedRate): void + public function testCalculateWithRulesByUsersHourlyRate(int $duration, array $rules, float $expectedRate): void { $end = self::createDateTime('12:00:00'); $start = clone $end; @@ -228,7 +225,7 @@ class RateServiceTest extends TestCase $record->setEnd($end); - $sut = new RateService($rules, $this->getRateRepositoryMock()); + $sut = $this->getSut($rules); $rate = $sut->calculate($record); self::assertEquals($expectedRate, $rate->getRate()); @@ -243,7 +240,7 @@ class RateServiceTest extends TestCase [ 31837, [], - 663 + 663.2708 ], [ 31837, @@ -257,7 +254,7 @@ class RateServiceTest extends TestCase 'factor' => 1.5 ], ], - 1326 // 8,84 * 75 (see user) * 2 + 1326.5417 ], [ 31837, @@ -271,7 +268,7 @@ class RateServiceTest extends TestCase 'factor' => 1.5 ], ], - 2320.5 // 75 * 8,84 * 3,5 + 2321.4479 ], ]; } diff --git a/tests/Timesheet/UtilTest.php b/tests/Timesheet/UtilTest.php index 8c0a977e..1e65cebe 100644 --- a/tests/Timesheet/UtilTest.php +++ b/tests/Timesheet/UtilTest.php @@ -12,11 +12,14 @@ namespace App\Tests\Timesheet; use App\Timesheet\Util; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; +#[Group('legacy')] #[CoversClass(Util::class)] class UtilTest extends TestCase { + #[Group('legacy')] #[DataProvider('getRateCalculationData')] public function testCalculateRate(float $hourlyRate, int $duration, float $expectedRate): void { @@ -42,6 +45,7 @@ class UtilTest extends TestCase yield [7360.99, 1234, 2502.74]; } + #[Group('legacy')] public function testCalculateRateWithRounding(): void { $total = 0.00; @@ -82,6 +86,7 @@ class UtilTest extends TestCase self::assertEqualsWithDelta(149176.3, $total, 0.00001); } + #[Group('legacy')] public function testDecimalDuration(): void { $inputs = [ diff --git a/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php b/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php index 5eaec05a..eaf27a99 100644 --- a/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php +++ b/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php @@ -27,6 +27,7 @@ use App\Project\ProjectStatisticService; use App\Repository\TimesheetRepository; use App\Tests\Mocks\SystemConfigurationFactory; use App\Timesheet\Rate; +use App\Timesheet\RateCalculator\ClassicRateCalculator; use App\Timesheet\RateService; use App\Timesheet\RateServiceInterface; use App\Validator\Constraints\TimesheetBudgetUsed; @@ -92,7 +93,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase $rateService = $this->createMock(RateServiceInterface::class); $rateService->method('calculate')->willReturn($rate); } else { - $rateService = new RateService([], $timesheetRepository); + $rateService = new RateService([], $timesheetRepository, new ClassicRateCalculator()); } $auth = $this->createMock(AuthorizationCheckerInterface::class); diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 3d999d88..164412f2 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -966,11 +966,6 @@ parameters: count: 1 path: Controller/TimesheetTeamControllerTest.php - - - message: "#^Parameter \\#2 \\$seconds of static method App\\\\Timesheet\\\\Util\\:\\:calculateRate\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: Controller/TimesheetTeamControllerTest.php - - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#" count: 2 @@ -1411,61 +1406,31 @@ parameters: count: 1 path: Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceItemDefaultHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelActivityHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#" count: 1 path: Invoice/Hydrator/InvoiceModelActivityHydratorTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelActivityHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Hydrator/InvoiceModelActivityHydratorTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelCustomerHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#" count: 1 path: Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelCustomerHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelDefaultHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#" count: 1 path: Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelDefaultHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelProjectHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#" count: 1 path: Invoice/Hydrator/InvoiceModelProjectHydratorTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelProjectHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Hydrator/InvoiceModelProjectHydratorTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelUserHydratorTest\\:\\:assertModelStructure\\(\\) has parameter \\$model with no value type specified in iterable type array\\.$#" count: 1 path: Invoice/Hydrator/InvoiceModelUserHydratorTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Hydrator\\\\InvoiceModelUserHydratorTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Hydrator/InvoiceModelUserHydratorTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\NumberGenerator\\\\DateNumberGeneratorTest\\:\\:getSut\\(\\) has no return type specified\\.$#" count: 1 @@ -1521,11 +1486,6 @@ parameters: count: 1 path: Invoice/Renderer/DebugRendererTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DebugRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Renderer/DebugRendererTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DebugRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#" count: 1 @@ -1576,16 +1536,6 @@ parameters: count: 1 path: Invoice/Renderer/DebugRendererTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\DocxRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Renderer/DocxRendererTest.php - - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\OdsRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Renderer/OdsRendererTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\OdsRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#" count: 1 @@ -1621,11 +1571,6 @@ parameters: count: 1 path: Invoice/Renderer/OdsRendererTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\PdfRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Renderer/PdfRendererTest.php - - message: "#^Parameter \\#2 \\$cacheDirectory of class App\\\\Pdf\\\\MPdfConverter constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" count: 1 @@ -1636,11 +1581,6 @@ parameters: count: 3 path: Invoice/Renderer/PdfRendererTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\TwigRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Renderer/TwigRendererTest.php - - message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#" count: 1 @@ -1651,11 +1591,6 @@ parameters: count: 3 path: Invoice/Renderer/TwigRendererTest.php - - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\XlsxRendererTest\\:\\:getAbstractRenderer\\(\\) should return App\\\\Invoice\\\\Renderer\\\\AbstractRenderer but returns object\\.$#" - count: 1 - path: Invoice/Renderer/XlsxRendererTest.php - - message: "#^Method App\\\\Tests\\\\Invoice\\\\Renderer\\\\XlsxRendererTest\\:\\:getTestModel\\(\\) has no return type specified\\.$#" count: 1 @@ -1731,20 +1666,11 @@ parameters: count: 1 path: Ldap/LdapManagerTest.php - - - message: "#^Method App\\\\Tests\\\\Mocks\\\\AbstractMockFactory\\:\\:createMock\\(\\) has no return type specified\\.$#" - count: 1 - path: Mocks/AbstractMockFactory.php - - message: "#^Method App\\\\Tests\\\\Mocks\\\\AbstractMockFactory\\:\\:getMockBuilder\\(\\) return type with generic class PHPUnit\\\\Framework\\\\MockObject\\\\MockBuilder does not specify its types\\: TMockedClass$#" count: 1 path: Mocks/AbstractMockFactory.php - - - message: "#^Parameter \\#2 \\$type of class PHPUnit\\\\Framework\\\\MockObject\\\\MockBuilder constructor expects class\\-string\\, string given\\.$#" - count: 1 - path: Mocks/AbstractMockFactory.php - message: "#^Parameter \\#1 \\$dataDir of class App\\\\Utils\\\\FileHelper constructor expects string, string\\|false given\\.$#" count: 1 @@ -1935,116 +1861,16 @@ parameters: count: 1 path: Timesheet/Calculator/DurationCalculatorTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getRateRepositoryMock\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:getRateTestData\\(\\) has no return type specified\\.$#" count: 1 path: Timesheet/Calculator/RateCalculatorTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$duration with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$expectedRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$rules with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has no return type specified\\.$#" count: 1 path: Timesheet/Calculator/RateCalculatorTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$activityInternal with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$activityIsFixed with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$activityRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$customerInternal with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$customerIsFixed with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$customerRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$duration with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$expectedInternalRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$expectedRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$projectInternal with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$projectIsFixed with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$projectRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$timesheetFixed with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$timesheetHourly with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$userInternalRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\Calculator\\\\RateCalculatorTest\\:\\:testRates\\(\\) has parameter \\$userRate with no type specified\\.$#" - count: 1 - path: Timesheet/Calculator/RateCalculatorTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\DateTimeFactoryTest\\:\\:getEndOfWeekData\\(\\) has no return type specified\\.$#" count: 1 @@ -2065,111 +1891,11 @@ parameters: count: 1 path: Timesheet/LockdownServiceTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getRateRepositoryMock\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:getRateTestData\\(\\) has no return type specified\\.$#" count: 1 path: Timesheet/RateServiceTest.php - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$duration with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$expectedRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testCalculateWithRulesByUsersHourlyRate\\(\\) has parameter \\$rules with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$activityInternal with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$activityIsFixed with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$activityRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$customerInternal with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$customerIsFixed with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$customerRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$duration with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$expectedInternalRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$expectedRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$projectInternal with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$projectIsFixed with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$projectRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$timesheetFixed with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$timesheetHourly with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$userInternalRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - - - message: "#^Method App\\\\Tests\\\\Timesheet\\\\RateServiceTest\\:\\:testRates\\(\\) has parameter \\$userRate with no type specified\\.$#" - count: 1 - path: Timesheet/RateServiceTest.php - - message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#" count: 6 @@ -2924,3 +2650,29 @@ parameters: message: "#^Method App\\\\Tests\\\\Widget\\\\Type\\\\UserDurationYearTest\\:\\:getDefaultOptions\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: Widget/Type/UserDurationYearTest.php + - + message: ''' + #^Access to constant on deprecated class App\\Timesheet\\Util\: + use RateCalculatorMode instead$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: Timesheet/UtilTest.php + + - + message: ''' + #^Call to method calculateRate\(\) of deprecated class App\\Timesheet\\Util\: + use RateCalculatorMode instead$# + ''' + identifier: staticMethod.deprecatedClass + count: 2 + path: Timesheet/UtilTest.php + + - + message: ''' + #^Call to method decimalizeDuration\(\) of deprecated class App\\Timesheet\\Util\: + use RateCalculatorMode instead$# + ''' + identifier: staticMethod.deprecatedClass + count: 1 + path: Timesheet/UtilTest.php diff --git a/translations/system-configuration.de.xlf b/translations/system-configuration.de.xlf index 7ee424ea..3f883229 100644 --- a/translations/system-configuration.de.xlf +++ b/translations/system-configuration.de.xlf @@ -310,6 +310,10 @@ copy_teams_on_create Übernehme Teams beim Erstellen neuer Einträge vom angemeldeten Benutzer + + invoice.rounding_mode + Rundungsmodus + diff --git a/translations/system-configuration.en.xlf b/translations/system-configuration.en.xlf index a89d2d07..d4c43093 100644 --- a/translations/system-configuration.en.xlf +++ b/translations/system-configuration.en.xlf @@ -310,6 +310,10 @@ copy_teams_on_create Take over teams from the logged-in user when creating new entries + + invoice.rounding_mode + Rounding mode +