Compare commits
3 Commits
2.2.1
...
customer-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1cc5f2587 | ||
|
|
6b672a23b4 | ||
|
|
900254b83a |
368
composer.lock
generated
368
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
when@prod:
|
||||
monolog:
|
||||
# channels: ["deprecation"]
|
||||
channels: ["deprecation"]
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
@@ -15,11 +15,10 @@ when@prod:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
# deactivated, because currently there are too many deprecations cause by gedmo and doctrine
|
||||
# deprecation:
|
||||
# type: stream
|
||||
# channels: ["deprecation"]
|
||||
# path: "%kernel.logs_dir%/deprecations.log"
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: ["deprecation"]
|
||||
path: "%kernel.logs_dir%/deprecations.log"
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
|
||||
@@ -60,7 +60,6 @@ security:
|
||||
logout:
|
||||
path: logout
|
||||
target: homepage
|
||||
enable_csrf: true
|
||||
|
||||
login_throttling:
|
||||
max_attempts: 5
|
||||
|
||||
45
phpstan.neon
45
phpstan.neon
@@ -1331,46 +1331,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'billable' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'counter' on mixed\\.$#"
|
||||
count: 3
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'duration' on mixed\\.$#"
|
||||
count: 4
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'exported' on mixed\\.$#"
|
||||
count: 2
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'id' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'internalRate' on mixed\\.$#"
|
||||
count: 3
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'rate' on mixed\\.$#"
|
||||
count: 4
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Customer\\\\CustomerStatisticService\\:\\:createStatisticQueryBuilder\\(\\) has parameter \\$customers with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Customer/CustomerStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Customer\\\\CustomerStatisticService\\:\\:getBudgetStatistic\\(\\) should return array\\<int, App\\\\Model\\\\CustomerStatistic\\> but returns array\\<int\\|string, App\\\\Model\\\\CustomerStatistic\\>\\.$#"
|
||||
count: 1
|
||||
@@ -2006,6 +1966,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$file of class Symfony\\\\Component\\\\HttpFoundation\\\\BinaryFileResponse constructor expects SplFileInfo\\|string, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Export/Base/AbstractSpreadsheetRenderer.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$id of method Symfony\\\\Contracts\\\\Translation\\\\TranslatorInterface\\:\\:trans\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 5
|
||||
|
||||
@@ -77,7 +77,7 @@ final class UserLoginLinkCommand extends Command
|
||||
$loginLink = $loginLinkDetails->getUrl();
|
||||
|
||||
if ($input->getOption('password-reset') === true) {
|
||||
$user->markPasswordRequested();
|
||||
$user->setPasswordRequestedAt(new \DateTime());
|
||||
$user->setRequiresPasswordReset(true);
|
||||
$this->userRepository->saveUser($user);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.2.1';
|
||||
public const VERSION = '2.1.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20201;
|
||||
public const VERSION_ID = 20100;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
53
src/Controller/Reporting/CustomerViewController.php
Normal file
53
src/Controller/Reporting/CustomerViewController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\Controller\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Customer\CustomerStatisticService;
|
||||
use App\Reporting\CustomerView\CustomerViewForm;
|
||||
use App\Reporting\CustomerView\CustomerViewQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class CustomerViewController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/reporting/customer_view', name: 'report_customer_view', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:customer')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'customer')"))]
|
||||
public function __invoke(Request $request, CustomerStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new CustomerViewQuery($dateFactory->createDateTime(), $user);
|
||||
$form = $this->createFormForGetRequest(CustomerViewForm::class, $query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$customers = $service->findCustomersForView($query);
|
||||
$entries = $service->getCustomerView($user, $customers, $query->getToday());
|
||||
|
||||
$byCustomer = [];
|
||||
foreach ($entries as $entry) {
|
||||
$customer = $entry->getCustomer();
|
||||
$byCustomer[$customer->getId()] = $entry;
|
||||
}
|
||||
|
||||
return $this->render('reporting/customer_view.html.twig', [
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
'report_title' => 'report_customer_view',
|
||||
'tableName' => 'customer_view_reporting',
|
||||
'now' => $dateFactory->createDateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use App\Event\EmailPasswordResetEvent;
|
||||
use App\Form\PasswordResetForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use DateTime;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -82,7 +83,7 @@ final class PasswordResetController extends AbstractController
|
||||
// this will finally send the email
|
||||
$this->eventDispatcher->dispatch(new EmailEvent($event->getEmail()));
|
||||
|
||||
$user->markPasswordRequested();
|
||||
$user->setPasswordRequestedAt(new DateTime());
|
||||
$this->userService->updateUser($user);
|
||||
}
|
||||
|
||||
@@ -137,7 +138,8 @@ final class PasswordResetController extends AbstractController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->markPasswordResetted();
|
||||
$user->setConfirmationToken(null);
|
||||
$user->setPasswordRequestedAt(null);
|
||||
$user->setEnabled(true);
|
||||
|
||||
$this->userService->updateUser($user);
|
||||
|
||||
@@ -11,23 +11,33 @@ namespace App\Customer;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Event\CustomerBudgetStatisticEvent;
|
||||
use App\Event\CustomerStatisticEvent;
|
||||
use App\Model\CustomerBudgetStatisticModel;
|
||||
use App\Model\CustomerStatistic;
|
||||
use App\Reporting\CustomerView\CustomerViewModel;
|
||||
use App\Reporting\CustomerView\CustomerViewQuery;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Loader\CustomerLoader;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class CustomerStatisticService
|
||||
{
|
||||
public function __construct(private TimesheetRepository $timesheetRepository, private EventDispatcherInterface $dispatcher)
|
||||
public function __construct(
|
||||
private CustomerRepository $customerRepository,
|
||||
private TimesheetRepository $timesheetRepository,
|
||||
private EventDispatcherInterface $dispatcher
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -85,6 +95,7 @@ class CustomerStatisticService
|
||||
$result = $qb->getQuery()->getResult();
|
||||
|
||||
if (null !== $result) {
|
||||
/** @var array{'id': string, 'duration': int, 'internalRate': float, 'counter': int, 'rate': float, 'billable': int, 'exported': int} $resultRow */
|
||||
foreach ($result as $resultRow) {
|
||||
$statistic = $statistics[$resultRow['id']];
|
||||
$statistic->setDuration($statistic->getDuration() + $resultRow['duration']);
|
||||
@@ -113,12 +124,16 @@ class CustomerStatisticService
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Customer> $customers
|
||||
*/
|
||||
private function createStatisticQueryBuilder(array $customers, DateTime $begin = null, ?DateTime $end = null): QueryBuilder
|
||||
{
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(p.customer) AS id')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->join(Customer::class, 'c', Query\Expr\Join::WITH, 'p.customer = c.id')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
|
||||
@@ -149,4 +164,177 @@ class CustomerStatisticService
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerViewQuery $query
|
||||
* @return Customer[]
|
||||
*/
|
||||
public function findCustomersForView(CustomerViewQuery $query): array
|
||||
{
|
||||
$user = $query->getUser();
|
||||
|
||||
$qb = $this->customerRepository->createQueryBuilder('c');
|
||||
$qb
|
||||
->select('c')
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->addGroupBy('c')
|
||||
;
|
||||
|
||||
if ($query->isIncludeWithBudget()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gt('c.timeBudget', 0),
|
||||
$qb->expr()->gt('c.budget', 0)
|
||||
)
|
||||
);
|
||||
} elseif ($query->isIncludeWithoutBudget()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('c.timeBudget', 0),
|
||||
$qb->expr()->eq('c.budget', 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->customerRepository->addPermissionCriteria($qb, $user);
|
||||
|
||||
/** @var Customer[] $customers */
|
||||
$customers = $qb->getQuery()->getResult();
|
||||
|
||||
// pre-cache customer objects instead of joining them
|
||||
$loader = new CustomerLoader($this->customerRepository->createQueryBuilder('c')->getEntityManager(), false);
|
||||
$loader->loadResults($customers);
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Customer[] $customers
|
||||
* @param DateTime $today
|
||||
* @return CustomerViewModel[]
|
||||
*/
|
||||
public function getCustomerView(User $user, array $customers, DateTime $today): array
|
||||
{
|
||||
$today = clone $today;
|
||||
|
||||
/** @var array<int, CustomerViewModel> $customerViews */
|
||||
$customerViews = [];
|
||||
foreach ($customers as $customer) {
|
||||
$customerViews[$customer->getId()] = new CustomerViewModel($customer);
|
||||
}
|
||||
|
||||
$budgetStats = $this->getBudgetStatisticModelForCustomers($customers, $today);
|
||||
foreach ($budgetStats as $model) {
|
||||
$customerViews[$model->getCustomer()->getId()]->setBudgetStatisticModel($model);
|
||||
}
|
||||
|
||||
$customerIds = array_keys($customerViews);
|
||||
|
||||
$tplQb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$tplQb
|
||||
->select('c.id AS id')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->join(Customer::class, 'c', Query\Expr\Join::WITH, 'p.customer = c.id')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) AS rate')
|
||||
->andWhere($tplQb->expr()->in('c.id', ':customer'))
|
||||
->groupBy('id')
|
||||
->setParameter('customer', array_values($customerIds))
|
||||
;
|
||||
|
||||
$qb = clone $tplQb;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
/** @var array{'duration': int, 'amount': int, 'rate': float, 'id': string, 'exported': int} $row */
|
||||
foreach ($result as $row) {
|
||||
$customerViews[$row['id']]->setDurationTotal($row['duration']);
|
||||
$customerViews[$row['id']]->setRateTotal($row['rate']);
|
||||
$customerViews[$row['id']]->setTimesheetCounter($row['amount']);
|
||||
}
|
||||
|
||||
$qb = clone $tplQb;
|
||||
$qb
|
||||
->addSelect('t.exported')
|
||||
->addSelect('t.billable')
|
||||
->addGroupBy('t.exported')
|
||||
->addGroupBy('t.billable')
|
||||
;
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
/** @var array{'duration': int, 'billable': int, 'rate': float, 'exported': int, 'id': string} $row */
|
||||
foreach ($result as $row) {
|
||||
$view = $customerViews[$row['id']];
|
||||
if ($row['billable'] === 1 && $row['exported'] === 1) {
|
||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
||||
} elseif ($row['billable'] === 1 && $row['exported'] === 0) {
|
||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
||||
$view->setNotBilledDuration($view->getNotBilledDuration() + $row['duration']);
|
||||
$view->setNotBilledRate($view->getNotBilledRate() + $row['rate']);
|
||||
} elseif ($row['billable'] === 0 && $row['exported'] === 0) {
|
||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
||||
}
|
||||
// the last possible case $row['billable'] === 0 && $row['exported'] === 1 is extremely unlikely and not used
|
||||
}
|
||||
|
||||
return array_values($customerViews);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer[] $customers
|
||||
* @param DateTime $today
|
||||
* @return CustomerBudgetStatisticModel[]
|
||||
*/
|
||||
public function getBudgetStatisticModelForCustomers(array $customers, DateTime $today): array
|
||||
{
|
||||
$models = [];
|
||||
$monthly = [];
|
||||
$allTime = [];
|
||||
|
||||
foreach ($customers as $customer) {
|
||||
$models[$customer->getId()] = new CustomerBudgetStatisticModel($customer);
|
||||
if ($customer->isMonthlyBudget()) {
|
||||
$monthly[] = $customer;
|
||||
} else {
|
||||
$allTime[] = $customer;
|
||||
}
|
||||
}
|
||||
|
||||
$statisticsTotal = $this->getBudgetStatistic($customers);
|
||||
foreach ($statisticsTotal as $id => $statistic) {
|
||||
$models[$id]->setStatisticTotal($statistic);
|
||||
}
|
||||
|
||||
$dateFactory = new DateTimeFactory($today->getTimezone());
|
||||
|
||||
$begin = null;
|
||||
$end = $today;
|
||||
|
||||
if (\count($monthly) > 0) {
|
||||
$begin = $dateFactory->getStartOfMonth($today);
|
||||
$end = $dateFactory->getEndOfMonth($today);
|
||||
$statistics = $this->getBudgetStatistic($monthly, $begin, $end);
|
||||
foreach ($statistics as $id => $statistic) {
|
||||
$models[$id]->setStatistic($statistic);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($allTime) > 0) {
|
||||
// display the budget at the end of the selected period and not the total sum of all times (do not include times in the future)
|
||||
$statistics = $this->getBudgetStatistic($allTime, null, $today);
|
||||
foreach ($statistics as $id => $statistic) {
|
||||
$models[$id]->setStatistic($statistic);
|
||||
}
|
||||
}
|
||||
|
||||
$event = new CustomerBudgetStatisticEvent($models, $begin, $end);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $models;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,8 +188,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
|
||||
#[Assert\NotNull]
|
||||
private ?string $category = self::WORK;
|
||||
#[ORM\Column(name: 'modified_at', type: 'datetime_immutable', nullable: true)]
|
||||
private \DateTimeImmutable $modifiedAt; // @phpstan-ignore-line - create migration and update all null values and then make it not null
|
||||
#[ORM\Column(name: 'modified_at', type: 'datetime', nullable: true)]
|
||||
private \DateTimeInterface $modifiedAt;
|
||||
/**
|
||||
* Tags
|
||||
*
|
||||
@@ -586,12 +586,12 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModifiedAt(): \DateTimeImmutable
|
||||
public function getModifiedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->modifiedAt;
|
||||
}
|
||||
|
||||
public function setModifiedAt(\DateTimeImmutable $dateTime): void
|
||||
public function setModifiedAt(\DateTimeInterface $dateTime): void
|
||||
{
|
||||
$this->modifiedAt = $dateTime;
|
||||
}
|
||||
|
||||
@@ -196,8 +196,8 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
#[ORM\Column(name: 'confirmation_token', type: 'string', length: 180, unique: true, nullable: true)]
|
||||
#[Assert\Length(max: 180)]
|
||||
private ?string $confirmationToken = null;
|
||||
#[ORM\Column(name: 'password_requested_at', type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $passwordRequestedAt = null;
|
||||
#[ORM\Column(name: 'password_requested_at', type: 'datetime', nullable: true)]
|
||||
private ?\DateTime $passwordRequestedAt = null;
|
||||
/**
|
||||
* List of all role names
|
||||
*/
|
||||
@@ -958,31 +958,26 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setConfirmationToken($confirmationToken): void
|
||||
public function setConfirmationToken($confirmationToken): User
|
||||
{
|
||||
$this->confirmationToken = $confirmationToken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markPasswordRequested(): void
|
||||
{
|
||||
$this->setPasswordRequestedAt(new \DateTimeImmutable('now', new \DateTimeZone($this->getTimezone())));
|
||||
}
|
||||
|
||||
public function markPasswordResetted(): void
|
||||
{
|
||||
$this->setConfirmationToken(null);
|
||||
$this->setPasswordRequestedAt(null);
|
||||
}
|
||||
|
||||
public function setPasswordRequestedAt(?\DateTimeImmutable $date): void
|
||||
public function setPasswordRequestedAt(?\DateTime $date = null): User
|
||||
{
|
||||
$this->passwordRequestedAt = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp that the user requested a password reset.
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function getPasswordRequestedAt(): ?\DateTimeImmutable
|
||||
public function getPasswordRequestedAt(): ?DateTime
|
||||
{
|
||||
return $this->passwordRequestedAt;
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ class WorkingTime
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?User $approvedBy = null;
|
||||
#[ORM\Column(name: 'approved_at', type: 'datetime_immutable', nullable: true)]
|
||||
#[ORM\Column(name: 'approved_at', type: 'datetime', nullable: true)]
|
||||
#[Assert\NotNull]
|
||||
private ?\DateTimeImmutable $approvedAt = null;
|
||||
private ?\DateTimeInterface $approvedAt = null;
|
||||
|
||||
public function __construct(User $user, \DateTimeInterface $date)
|
||||
{
|
||||
@@ -96,12 +96,12 @@ class WorkingTime
|
||||
$this->approvedBy = $approvedBy;
|
||||
}
|
||||
|
||||
public function getApprovedAt(): ?\DateTimeImmutable
|
||||
public function getApprovedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->approvedAt;
|
||||
}
|
||||
|
||||
public function setApprovedAt(?\DateTimeImmutable $approvedAt): void
|
||||
public function setApprovedAt(?\DateTimeInterface $approvedAt): void
|
||||
{
|
||||
$this->approvedAt = $approvedAt;
|
||||
}
|
||||
|
||||
57
src/Event/CustomerBudgetStatisticEvent.php
Normal file
57
src/Event/CustomerBudgetStatisticEvent.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Model\CustomerBudgetStatisticModel;
|
||||
|
||||
final class CustomerBudgetStatisticEvent
|
||||
{
|
||||
/**
|
||||
* @param CustomerBudgetStatisticModel[] $models
|
||||
* @param \DateTime|null $begin
|
||||
* @param \DateTime|null $end
|
||||
*/
|
||||
public function __construct(private array $models, private ?\DateTime $begin = null, private ?\DateTime $end = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function getModel(int $customerId): ?CustomerBudgetStatisticModel
|
||||
{
|
||||
if (isset($this->models[$customerId])) {
|
||||
return $this->models[$customerId];
|
||||
}
|
||||
|
||||
foreach ($this->models as $model) {
|
||||
if ($model->getCustomer()->getId() === $customerId) {
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CustomerBudgetStatisticModel[]
|
||||
*/
|
||||
public function getModels(): array
|
||||
{
|
||||
return $this->models;
|
||||
}
|
||||
|
||||
public function getBegin(): ?\DateTime
|
||||
{
|
||||
return $this->begin;
|
||||
}
|
||||
|
||||
public function getEnd(): ?\DateTime
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
}
|
||||
@@ -215,7 +215,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
|
||||
protected function setRate(Worksheet $sheet, int $column, int $row, ?float $rate, ?string $currency): void
|
||||
{
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $rate ?? 0.0);
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $rate ?? 0);
|
||||
$this->setRateStyle($sheet, $column, $row, $currency);
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
};
|
||||
}
|
||||
if (!isset($columns['username']['header'])) {
|
||||
$columns['username']['header'] = function (Worksheet $sheet, int $row, int $column): int {
|
||||
$columns['username']['header'] = function (Worksheet $sheet, $row, $column): int {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('name'));
|
||||
|
||||
return 1;
|
||||
@@ -426,7 +426,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
|
||||
|
||||
$columns['timesheet-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($timesheetMetaFields): int {
|
||||
'header' => function (Worksheet $sheet, $row, $column) use ($timesheetMetaFields): int {
|
||||
foreach ($timesheetMetaFields as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
@@ -454,7 +454,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($customerQuery, CustomerMetaDisplayEvent::EXPORT));
|
||||
|
||||
$columns['customer-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($customerMetaFields): int {
|
||||
'header' => function (Worksheet $sheet, $row, $column) use ($customerMetaFields): int {
|
||||
foreach ($customerMetaFields as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
@@ -481,7 +481,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
if (isset($columns['project-meta'])) {
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
|
||||
$columns['project-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($projectMetaFields): int {
|
||||
'header' => function (Worksheet $sheet, $row, $column) use ($projectMetaFields): int {
|
||||
foreach ($projectMetaFields as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
@@ -508,7 +508,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
if (isset($columns['activity-meta'])) {
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
|
||||
$columns['activity-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($activityMetaFields): int {
|
||||
'header' => function (Worksheet $sheet, $row, $column) use ($activityMetaFields): int {
|
||||
foreach ($activityMetaFields as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
@@ -537,7 +537,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$this->dispatcher->dispatch($event);
|
||||
$userPreferences = $event->getPreferences();
|
||||
$columns['user-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($userPreferences): int {
|
||||
'header' => function (Worksheet $sheet, $row, $column) use ($userPreferences): int {
|
||||
foreach ($userPreferences as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
@@ -575,7 +575,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
|
||||
if (isset($columns['customer_number'])) {
|
||||
if (!isset($columns['customer_number']['header'])) {
|
||||
$columns['customer_number']['header'] = function (Worksheet $sheet, int $row, int $column): int {
|
||||
$columns['customer_number']['header'] = function (Worksheet $sheet, $row, $column): int {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('number'));
|
||||
|
||||
return 1;
|
||||
@@ -595,7 +595,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
|
||||
if (isset($columns['customer_vat']) && !isset($columns['customer_vat']['render'])) {
|
||||
if (!isset($columns['customer_vat']['header'])) {
|
||||
$columns['customer_vat']['header'] = function (Worksheet $sheet, int $row, int $column): int {
|
||||
$columns['customer_vat']['header'] = function (Worksheet $sheet, $row, $column): int {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('vat_id'));
|
||||
|
||||
return 1;
|
||||
@@ -615,7 +615,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
|
||||
if (isset($columns['order_number']) && !isset($columns['order_number']['render'])) {
|
||||
if (!isset($columns['order_number']['header'])) {
|
||||
$columns['order_number']['header'] = function (Worksheet $sheet, int $row, int $column): int {
|
||||
$columns['order_number']['header'] = function (Worksheet $sheet, $row, $column): int {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('orderNumber'));
|
||||
|
||||
return 1;
|
||||
@@ -767,11 +767,11 @@ abstract class AbstractSpreadsheetRenderer
|
||||
abstract public function getFileExtension(): string;
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @param mixed $file
|
||||
* @param string $filename
|
||||
* @return BinaryFileResponse
|
||||
*/
|
||||
protected function getFileResponse(string $file, string $filename): BinaryFileResponse
|
||||
protected function getFileResponse($file, $filename): BinaryFileResponse
|
||||
{
|
||||
$response = new BinaryFileResponse($file);
|
||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||
|
||||
@@ -16,7 +16,7 @@ use Symfony\Component\Form\AbstractType;
|
||||
*/
|
||||
final class ContractByUserForm extends AbstractType
|
||||
{
|
||||
public function getParent(): string
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return YearByUserForm::class;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
|
||||
class Day
|
||||
{
|
||||
public function __construct(private DateTimeImmutable $day)
|
||||
public function __construct(private DateTimeInterface $day)
|
||||
{
|
||||
}
|
||||
|
||||
public function getDay(): DateTimeImmutable
|
||||
public function getDay(): DateTimeInterface
|
||||
{
|
||||
return $this->day;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class Month
|
||||
|
||||
public function __construct(private \DateTimeInterface $month)
|
||||
{
|
||||
$date = new \DateTimeImmutable($this->month->format('Y-m-01 00:00:00'), $month->getTimezone());
|
||||
$date = new \DateTimeImmutable($this->month->format('Y-m-01 00:00:00'));
|
||||
$start = $date->format('m');
|
||||
while ($start === $date->format('m')) {
|
||||
$day = $this->createDay($date);
|
||||
@@ -29,7 +29,7 @@ class Month
|
||||
}
|
||||
}
|
||||
|
||||
protected function createDay(\DateTimeImmutable $day): Day
|
||||
protected function createDay(\DateTimeInterface $day): Day
|
||||
{
|
||||
return new Day($day);
|
||||
}
|
||||
|
||||
@@ -20,15 +20,17 @@ class Year
|
||||
|
||||
public function __construct(private DateTimeInterface $month)
|
||||
{
|
||||
$monthDate = new \DateTimeImmutable($this->month->format('Y-01-01 01:00:00'), $this->month->getTimezone());
|
||||
$monthDate = new \DateTimeImmutable();
|
||||
$monthDate = $monthDate->setDate((int) $this->month->format('Y'), 1, 1);
|
||||
$monthDate = $monthDate->setTime(1, 0);
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
$tmp = $this->createMonth($monthDate);
|
||||
$this->setMonth($tmp);
|
||||
$month = $this->createMonth($monthDate);
|
||||
$this->setMonth($month);
|
||||
$monthDate = $monthDate->add(new \DateInterval('P1M'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function createMonth(\DateTimeImmutable $month): Month
|
||||
protected function createMonth(\DateTimeInterface $month): Month
|
||||
{
|
||||
return new Month($month);
|
||||
}
|
||||
|
||||
45
src/Reporting/CustomerView/CustomerViewForm.php
Normal file
45
src/Reporting/CustomerView/CustomerViewForm.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\Reporting\CustomerView;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<CustomerViewQuery>
|
||||
*/
|
||||
final class CustomerViewForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('budgetType', ChoiceType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'expanded' => true,
|
||||
'choices' => [
|
||||
'all' => null,
|
||||
'includeWithBudget' => true,
|
||||
'includeNoBudget' => false
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => CustomerViewQuery::class,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
136
src/Reporting/CustomerView/CustomerViewModel.php
Normal file
136
src/Reporting/CustomerView/CustomerViewModel.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?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\Reporting\CustomerView;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Model\BudgetStatisticModelInterface;
|
||||
|
||||
final class CustomerViewModel
|
||||
{
|
||||
private int $timesheetCounter = 0;
|
||||
private int $durationTotal = 0;
|
||||
private float $rateTotal = 0.00;
|
||||
private int $notExportedDuration = 0;
|
||||
private float $notExportedRate = 0.00;
|
||||
private int $notBilledDuration = 0;
|
||||
private float $notBilledRate = 0.00;
|
||||
private int $billableDuration = 0;
|
||||
private float $billableRate = 0.00;
|
||||
private ?BudgetStatisticModelInterface $budgetStatisticModel = null;
|
||||
|
||||
public function __construct(private Customer $customer)
|
||||
{
|
||||
}
|
||||
|
||||
public function getCustomer(): Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function getTimesheetCounter(): int
|
||||
{
|
||||
return $this->timesheetCounter;
|
||||
}
|
||||
|
||||
public function setTimesheetCounter(int $timesheetCounter): void
|
||||
{
|
||||
$this->timesheetCounter = $timesheetCounter;
|
||||
}
|
||||
|
||||
public function getDurationTotal(): int
|
||||
{
|
||||
return $this->durationTotal;
|
||||
}
|
||||
|
||||
public function setDurationTotal(int $durationTotal): void
|
||||
{
|
||||
$this->durationTotal = $durationTotal;
|
||||
}
|
||||
|
||||
public function getNotExportedDuration(): int
|
||||
{
|
||||
return $this->notExportedDuration;
|
||||
}
|
||||
|
||||
public function setNotExportedDuration(int $notExportedDuration): void
|
||||
{
|
||||
$this->notExportedDuration = $notExportedDuration;
|
||||
}
|
||||
|
||||
public function getNotExportedRate(): float
|
||||
{
|
||||
return $this->notExportedRate;
|
||||
}
|
||||
|
||||
public function setNotExportedRate(float $notExportedRate): void
|
||||
{
|
||||
$this->notExportedRate = $notExportedRate;
|
||||
}
|
||||
|
||||
public function getNotBilledDuration(): int
|
||||
{
|
||||
return $this->notBilledDuration;
|
||||
}
|
||||
|
||||
public function setNotBilledDuration(int $notBilledDuration): void
|
||||
{
|
||||
$this->notBilledDuration = $notBilledDuration;
|
||||
}
|
||||
|
||||
public function getNotBilledRate(): float
|
||||
{
|
||||
return $this->notBilledRate;
|
||||
}
|
||||
|
||||
public function setNotBilledRate(float $notBilledRate): void
|
||||
{
|
||||
$this->notBilledRate = $notBilledRate;
|
||||
}
|
||||
|
||||
public function getBillableDuration(): int
|
||||
{
|
||||
return $this->billableDuration;
|
||||
}
|
||||
|
||||
public function setBillableDuration(int $billableDuration): void
|
||||
{
|
||||
$this->billableDuration = $billableDuration;
|
||||
}
|
||||
|
||||
public function getBillableRate(): float
|
||||
{
|
||||
return $this->billableRate;
|
||||
}
|
||||
|
||||
public function setBillableRate(float $billableRate): void
|
||||
{
|
||||
$this->billableRate = $billableRate;
|
||||
}
|
||||
|
||||
public function getRateTotal(): float
|
||||
{
|
||||
return $this->rateTotal;
|
||||
}
|
||||
|
||||
public function setRateTotal(float $rateTotal): void
|
||||
{
|
||||
$this->rateTotal = $rateTotal;
|
||||
}
|
||||
|
||||
public function getBudgetStatisticModel(): BudgetStatisticModelInterface
|
||||
{
|
||||
return $this->budgetStatisticModel; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function setBudgetStatisticModel(BudgetStatisticModelInterface $budgetStatisticModel): void
|
||||
{
|
||||
$this->budgetStatisticModel = $budgetStatisticModel;
|
||||
}
|
||||
}
|
||||
55
src/Reporting/CustomerView/CustomerViewQuery.php
Normal file
55
src/Reporting/CustomerView/CustomerViewQuery.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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\Reporting\CustomerView;
|
||||
|
||||
use App\Entity\User;
|
||||
use DateTime;
|
||||
|
||||
final class CustomerViewQuery
|
||||
{
|
||||
private ?bool $budgetType = true;
|
||||
|
||||
public function __construct(private DateTime $today, private User $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getBudgetType(): ?bool
|
||||
{
|
||||
return $this->budgetType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setBudgetType(?bool $budgetType): void
|
||||
{
|
||||
$this->budgetType = $budgetType;
|
||||
}
|
||||
|
||||
public function isIncludeWithoutBudget(): bool
|
||||
{
|
||||
return $this->budgetType === false;
|
||||
}
|
||||
|
||||
public function isIncludeWithBudget(): bool
|
||||
{
|
||||
return $this->budgetType === true;
|
||||
}
|
||||
|
||||
public function getToday(): DateTime
|
||||
{
|
||||
return $this->today;
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ final class ReportingService
|
||||
|
||||
if ($this->security->isGranted('report:customer')) {
|
||||
if ($viewOther) {
|
||||
$event->addReport(new Report('customer_view', 'report_customer_view', 'report_customer_view', 'customer'));
|
||||
$event->addReport(new Report('report_customer_monthly_projects', 'report_customer_monthly_projects', 'report_customer_monthly_projects', 'customer'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ class CustomerRepository extends EntityRepository
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
$permissions = $this->getPermissionCriteria($qb, $user, $teams);
|
||||
if ($permissions->count() > 0) {
|
||||
|
||||
@@ -31,113 +31,14 @@ final class InvoicePolicy implements SecurityPolicyInterface
|
||||
$this->policy->addPolicy(new SecurityPolicy(
|
||||
['block', 'if', 'for', 'set', 'extends'],
|
||||
[
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
// Twig core filters
|
||||
'map', 'escape', 'trans', 'default', 'nl2br', 'trim', 'raw',
|
||||
'join', 'u', 'slice', 'date', 'month_name', 'first', 'country_name',
|
||||
'replace', 'length', 'number_format', 'split',
|
||||
|
||||
// formatting filters
|
||||
'date',
|
||||
'date_modify',
|
||||
'format',
|
||||
'replace',
|
||||
'number_format',
|
||||
'abs',
|
||||
'round',
|
||||
|
||||
// encoding
|
||||
'url_encode',
|
||||
'json_encode',
|
||||
'convert_encoding',
|
||||
|
||||
// string filters
|
||||
'title',
|
||||
'capitalize',
|
||||
'upper',
|
||||
'lower',
|
||||
'striptags',
|
||||
'trim',
|
||||
'nl2br',
|
||||
'spaceless',
|
||||
|
||||
// array helpers
|
||||
'join',
|
||||
'split',
|
||||
'sort',
|
||||
'merge',
|
||||
'batch',
|
||||
'column',
|
||||
'filter',
|
||||
'map',
|
||||
'reduce',
|
||||
|
||||
// string/array filters
|
||||
'reverse',
|
||||
'length',
|
||||
'slice',
|
||||
'first',
|
||||
'last',
|
||||
|
||||
// iteration and runtime
|
||||
'default',
|
||||
'keys',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/EscaperExtension.php
|
||||
'escape',
|
||||
'e',
|
||||
'raw',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
'trans',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/string-extra/StringExtension.php
|
||||
'u',
|
||||
'slug',
|
||||
|
||||
// =================================================================
|
||||
// vendor/twig/intl-extra/IntlExtension.php
|
||||
'country_name',
|
||||
'currency_name',
|
||||
'currency_symbol',
|
||||
'language_name',
|
||||
'locale_name',
|
||||
'format_currency',
|
||||
'format_number',
|
||||
'format_*_number',
|
||||
'format_datetime',
|
||||
'format_date',
|
||||
'format_time',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/LocaleFormatExtensions.php
|
||||
'month_name',
|
||||
'day_name',
|
||||
'date_short',
|
||||
'date_time',
|
||||
'date_full',
|
||||
'date_format',
|
||||
'date_weekday',
|
||||
'time',
|
||||
'duration',
|
||||
'duration_decimal',
|
||||
'money',
|
||||
'amount',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/RuntimeExtensions.php
|
||||
'md2html',
|
||||
'desc2html',
|
||||
'comment2html',
|
||||
'comment1line',
|
||||
|
||||
// =================================================================
|
||||
// src/Twig/Extensions.php
|
||||
'multiline_indent',
|
||||
'color',
|
||||
'font_contrast',
|
||||
'default_color',
|
||||
'nl2str',
|
||||
// Kimai filters
|
||||
'md2html', 'desc2html', 'comment2html', 'comment1line', 'multiline_indent', 'nl2str',
|
||||
'date_short', 'duration', 'amount', 'money', 'duration_decimal',
|
||||
],
|
||||
[
|
||||
PdfContext::class => ['setoption'],
|
||||
@@ -145,47 +46,11 @@ final class InvoicePolicy implements SecurityPolicyInterface
|
||||
],
|
||||
[], // properties
|
||||
[
|
||||
// =================================================================
|
||||
// vendor/twig/twig/src/Extension/CoreExtension.php
|
||||
'max',
|
||||
'min',
|
||||
'range',
|
||||
'constant',
|
||||
'cycle',
|
||||
'random',
|
||||
'date',
|
||||
'asset',
|
||||
'range',
|
||||
// Twig core functions
|
||||
'cycle', 'asset', 'range',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/TranslationExtension.php
|
||||
't',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/webpack-encore-bundle/src/Twig/EntryFilesTwigExtension.php
|
||||
'encore_entry_css_source',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/AssetExtension.php
|
||||
'asset',
|
||||
|
||||
// =================================================================
|
||||
// vendor/symfony/twig-bridge/Extension/SecurityExtension.php
|
||||
'is_granted',
|
||||
|
||||
// =================================================================
|
||||
// Twig/RuntimeExtensions.php
|
||||
'qr_code_data_uri',
|
||||
|
||||
// =================================================================
|
||||
// Twig/Configuration.php
|
||||
'config',
|
||||
|
||||
// =================================================================
|
||||
// Twig/LocaleFormatExtensions.php
|
||||
'create_date',
|
||||
'month_names',
|
||||
'locale_format',
|
||||
// Kimai functions
|
||||
'encore_entry_css_source', 'qr_code_data_uri', 'config',
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use App\Model\Month as BaseMonth;
|
||||
*/
|
||||
final class Month extends BaseMonth
|
||||
{
|
||||
public function __construct(\DateTimeImmutable $month, private User $user)
|
||||
public function __construct(\DateTimeInterface $month, private User $user)
|
||||
{
|
||||
parent::__construct($month);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ final class Month extends BaseMonth
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function createDay(\DateTimeImmutable $day): Day
|
||||
protected function createDay(\DateTimeInterface $day): Day
|
||||
{
|
||||
return new Day($day);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ final class Year extends BaseYear
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
protected function createMonth(\DateTimeImmutable $month): Month
|
||||
protected function createMonth(\DateTimeInterface $month): Month
|
||||
{
|
||||
return new Month($month, $this->user);
|
||||
}
|
||||
|
||||
@@ -117,8 +117,7 @@ final class WorkingTimeService
|
||||
}
|
||||
|
||||
$workingTime->setApprovedBy($approver);
|
||||
// FIXME see calling method
|
||||
$workingTime->setApprovedAt(\DateTimeImmutable::createFromInterface($approvalDate));
|
||||
$workingTime->setApprovedAt($approvalDate);
|
||||
$this->workingTimeRepository->scheduleWorkingTimeUpdate($workingTime);
|
||||
}
|
||||
|
||||
|
||||
128
templates/reporting/customer_list_data.html.twig
Normal file
128
templates/reporting/customer_list_data.html.twig
Normal file
@@ -0,0 +1,128 @@
|
||||
{% extends 'reporting/layout.html.twig' %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
|
||||
{% set showMoneyBudget = is_granted('budget_money', 'customer') %}
|
||||
{% set showTimeBudget = is_granted('budget_time', 'customer') %}
|
||||
|
||||
{% set availableColumns = {
|
||||
'name': {'class': 'alwaysVisible'},
|
||||
} %}
|
||||
{% if showTimeBudget %}
|
||||
{% set availableColumns = availableColumns|merge({
|
||||
'timeBudget': {'class': 'd-none d-md-table-cell', 'title': 'timeBudget'|trans},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% if showMoneyBudget %}
|
||||
{% set availableColumns = availableColumns|merge({
|
||||
'budget': {'class': 'd-none d-md-table-cell', 'title': 'budget'|trans},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% if showTimeBudget %}
|
||||
{% set availableColumns = availableColumns|merge({
|
||||
'durationTotal': {'class': 'text-end hw-min w-min', 'title': 'stats.durationTotal'|trans, 'columnClass': 'w-min'},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% if showTimeBudget %}
|
||||
{% set availableColumns = availableColumns|merge({
|
||||
'rateTotal': {'class': 'text-end hw-min w-min', 'title': 'stats.amountTotal'|trans, 'columnClass': 'w-min'},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% if showTimeBudget and is_granted('create_export') %}
|
||||
{% set availableColumns = availableColumns|merge({
|
||||
'exported': {'class': 'd-none d-xl-table-cell text-end hw-min w-min', 'title': 'not_exported'|trans, 'columnClass': 'w-min'},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% if showMoneyBudget and is_granted('view_invoice') %}
|
||||
{% set availableColumns = availableColumns|merge({
|
||||
'invoiced': {'class': 'd-none d-xl-table-cell text-end hw-min w-min', 'title': 'not_invoiced'|trans, 'columnClass': 'w-min'},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set availableColumns = availableColumns|merge({
|
||||
'comment': {'class': 'd-none', 'title': 'comment'|trans},
|
||||
'actions': {'class': 'actions alwaysVisible'},
|
||||
}) %}
|
||||
{% set tableName = tableName|default('customer_view_reporting') %}
|
||||
{% set skipColumns = skipColumns is defined ? skipColumns : {} %}
|
||||
{% set columns = {} %}
|
||||
{% for name, config in availableColumns %}
|
||||
{% if name not in skipColumns %}
|
||||
{% set columns = columns|merge({(name): config}) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block report %}
|
||||
|
||||
{% set hasData = entries|length > 0 %}
|
||||
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "customer/actions.html.twig" as customerActions %}
|
||||
{% block box_body_class %}{{ tableName }}-box {% if hasData %}p-0{% endif %}{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if not hasData %}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, null, {boxClass: ''}) }}
|
||||
|
||||
{% for id, entry in entries|sort((a, b) => a.customer.name <=> b.customer.name) %}
|
||||
{% set customer = entry.customer %}
|
||||
{% set budgetStats = entry.getBudgetStatisticModel() %}
|
||||
{% set currency = customer.currency %}
|
||||
<tr {{ widgets.customer_row_attr(customer, now) }}>
|
||||
{% for name, column_config in columns %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
|
||||
{% if name == 'name' %}
|
||||
{{ widgets.label_customer(customer) }}
|
||||
{% elseif name == 'lastRecord' %}
|
||||
{% if entry.lastRecord is not null %}
|
||||
{{ entry.lastRecord|date_short }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
{% elseif name == 'today' %}
|
||||
{{ entry.durationDay|duration }}
|
||||
{% elseif name == 'week' %}
|
||||
{{ entry.durationWeek|duration }}
|
||||
{% elseif name == 'month' %}
|
||||
{{ entry.durationMonth|duration }}
|
||||
{% elseif name == 'durationTotal' %}
|
||||
{{ entry.durationTotal|duration }}
|
||||
{% elseif name == 'rateTotal' %}
|
||||
{{ entry.budgetStatisticModel.rateBillableTotal|money(currency) }}
|
||||
{% elseif name == 'timeBudget' %}
|
||||
{% if budgetStats.hasTimeBudget() and is_granted('time', customer) %}
|
||||
{{ progress.progressbar_timebudget(budgetStats) }}
|
||||
{% endif %}
|
||||
{% elseif name == 'budget' %}
|
||||
{% if customer.hasBudget() and is_granted('budget', customer) %}
|
||||
{{ progress.progressbar_budget(budgetStats, customer.currency) }}
|
||||
{% endif %}
|
||||
{% elseif name == 'exported' %}
|
||||
<a href="{{ path('export', {'customers[]': customer.id, 'daterange': '', 'preview': true}) }}">
|
||||
{{ entry.notExportedDuration|duration }}
|
||||
</a>
|
||||
{% elseif name == 'invoiced' %}
|
||||
<a href="{{ path('invoice', {'customers[]': customer.id, 'daterange': ''}) }}">
|
||||
{{ entry.notBilledRate|money(currency) }}
|
||||
</a>
|
||||
{% elseif name == 'comment' %}
|
||||
{{ customer.comment }}
|
||||
{% elseif name == 'actions' %}
|
||||
{{ customerActions.customer(customer, 'custom') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
16
templates/reporting/customer_view.html.twig
Normal file
16
templates/reporting/customer_view.html.twig
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends 'reporting/customer_list_data.html.twig' %}
|
||||
|
||||
{% block report_form_layout %}
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ icon('filter', true) }}
|
||||
</button>
|
||||
<ul class="dropdown-menu checkbox-menu">
|
||||
{% for option in form.budgetType.children %}
|
||||
<li class="dropdown-item">
|
||||
{{ form_widget(option) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -18,10 +18,10 @@
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set availableColumns = availableColumns|merge({
|
||||
'lastRecord': {'class': 'd-none d-sm-table-cell text-start hw-min w-min', 'title': 'last_record'|trans, 'columnClass': 'w-min'},
|
||||
'lastRecord': {'class': 'd-none text-start hw-min w-min', 'title': 'last_record'|trans, 'columnClass': 'w-min'},
|
||||
'today': {'class': 'd-none text-end hw-min w-min', 'title': 'stats.durationToday'|trans},
|
||||
'week': {'class': 'd-none text-end hw-min w-min', 'title': 'stats.durationWeek'|trans},
|
||||
'month': {'class': 'd-none d-lg-table-cell text-end hw-min w-min', 'title': 'stats.durationMonth'|trans},
|
||||
'month': {'class': 'd-none text-end hw-min w-min', 'title': 'stats.durationMonth'|trans},
|
||||
'durationTotal': {'class': 'text-end hw-min w-min', 'title': 'stats.durationTotal'|trans, 'columnClass': 'w-min'},
|
||||
}) %}
|
||||
{% if showTimeBudget and is_granted('create_export') %}
|
||||
|
||||
@@ -24,17 +24,14 @@ class SystemConfigurationTest extends TestCase
|
||||
* @param array $loaderSettings
|
||||
* @return SystemConfiguration
|
||||
*/
|
||||
protected function getSut(array $settings, array $loaderSettings = []): SystemConfiguration
|
||||
protected function getSut(array $settings, array $loaderSettings = [])
|
||||
{
|
||||
$loader = new TestConfigLoader($loaderSettings);
|
||||
|
||||
return SystemConfigurationFactory::create($loader, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<mixed>>
|
||||
*/
|
||||
protected function getDefaultSettings(): array
|
||||
protected function getDefaultSettings()
|
||||
{
|
||||
return [
|
||||
'timesheet' => [
|
||||
@@ -109,10 +106,7 @@ class SystemConfigurationTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Configuration>
|
||||
*/
|
||||
protected function getDefaultLoaderSettings(): array
|
||||
protected function getDefaultLoaderSettings()
|
||||
{
|
||||
return [
|
||||
(new Configuration())->setName('defaults.customer.timezone')->setValue('Russia/Moscov'),
|
||||
@@ -129,7 +123,7 @@ class SystemConfigurationTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
public function testDefaultWithoutLoader(): void
|
||||
public function testDefaultWithoutLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||
$this->assertEquals('Europe/London', $sut->find('defaults.customer.timezone'));
|
||||
@@ -139,7 +133,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals('Maroon|#800000,Brown|#a52a2a,Red|#ff0000,Orange|#ffa500,#ffffff,,|#000000', $sut->getThemeColorChoices());
|
||||
}
|
||||
|
||||
public function testDefaultWithLoader(): void
|
||||
public function testDefaultWithLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
|
||||
$this->assertEquals('Russia/Moscov', $sut->find('defaults.customer.timezone'));
|
||||
@@ -149,7 +143,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertFalse($sut->isSamlActive());
|
||||
}
|
||||
|
||||
public function testDefaultWithMixedConfigs(): void
|
||||
public function testDefaultWithMixedConfigs()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||
(new Configuration())->setName('timesheet.rules.allow_future_times')->setValue(''),
|
||||
@@ -163,7 +157,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals('2020-03-27', $sut->getFinancialYearStart());
|
||||
}
|
||||
|
||||
public function testOffsetUnsetThrowsException(): void
|
||||
public function testOffsetUnsetThrowsException()
|
||||
{
|
||||
$this->expectException(\BadMethodCallException::class);
|
||||
$this->expectExceptionMessage('SystemBundleConfiguration does not support offsetUnset()');
|
||||
@@ -172,7 +166,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$sut->offsetUnset('dfsdf');
|
||||
}
|
||||
|
||||
public function testUnknownConfigs(): void
|
||||
public function testUnknownConfigs()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||
(new Configuration())->setName('timesheet.foo')->setValue('hello'),
|
||||
@@ -188,7 +182,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals('foooo-bar!', $sut->find('xxxxxxxx.yyyyyyyyy'));
|
||||
}
|
||||
|
||||
public function testCalendarWithoutLoader(): void
|
||||
public function testCalendarWithoutLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||
$this->assertEquals('07:49', $sut->getCalendarBusinessTimeBegin());
|
||||
@@ -205,7 +199,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals(2, \count($sources));
|
||||
}
|
||||
|
||||
public function testCalendarWithLoader(): void
|
||||
public function testCalendarWithLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
|
||||
$this->assertEquals('00:30:00', $sut->getCalendarSlotDuration());
|
||||
@@ -213,7 +207,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals(2, \count($sources));
|
||||
}
|
||||
|
||||
public function testFormDefaultWithoutLoader(): void
|
||||
public function testFormDefaultWithoutLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||
$this->assertEquals('Europe/London', $sut->getCustomerDefaultTimezone());
|
||||
@@ -226,7 +220,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertNull($sut->getFinancialYearStart());
|
||||
}
|
||||
|
||||
public function testFormDefaultWithLoader(): void
|
||||
public function testFormDefaultWithLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
|
||||
$this->assertEquals('Russia/Moscov', $sut->getCustomerDefaultTimezone());
|
||||
@@ -238,7 +232,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals('USD', $sut->getUserDefaultCurrency());
|
||||
}
|
||||
|
||||
public function testTimesheetWithoutLoader(): void
|
||||
public function testTimesheetWithoutLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||
$this->assertEquals(99, $sut->getTimesheetActiveEntriesHardLimit());
|
||||
@@ -256,7 +250,7 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals(5, $sut->getTimesheetIncrementMinutes());
|
||||
}
|
||||
|
||||
public function testTimesheetWithLoader(): void
|
||||
public function testTimesheetWithLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
|
||||
$this->assertEquals(7, $sut->getTimesheetActiveEntriesHardLimit());
|
||||
|
||||
@@ -38,7 +38,7 @@ class LayoutControllerTest extends ControllerBaseTest
|
||||
$this->assertStringContainsString('href="/en/profile/' . $user->getUserIdentifier() . '"', $content);
|
||||
$this->assertStringContainsString('href="/en/profile/' . $user->getUserIdentifier() . '/edit"', $content);
|
||||
$this->assertStringContainsString('href="/en/profile/' . $user->getUserIdentifier() . '/prefs"', $content);
|
||||
$this->assertStringContainsString('href="/en/logout?_csrf_token=', $content);
|
||||
$this->assertStringContainsString('href="/en/logout"', $content);
|
||||
}
|
||||
|
||||
protected function assertHasNavigation(HttpKernelBrowser $client)
|
||||
|
||||
@@ -26,7 +26,7 @@ class ReportingControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/reporting/');
|
||||
$nodes = $client->getCrawler()->filter('section.content div.row-cards a.card-link');
|
||||
$this->assertCount(11, $nodes);
|
||||
$this->assertCount(12, $nodes);
|
||||
}
|
||||
|
||||
public function testOverviewPageAsUser()
|
||||
|
||||
@@ -183,7 +183,7 @@ class TimesheetTest extends TestCase
|
||||
$sut->setExported(true);
|
||||
$sut->setDescription('Invalid timesheet category "foo" given, expected one of: work, holiday, sickness, parental, overtime');
|
||||
|
||||
$modifiedDate = new \DateTimeImmutable();
|
||||
$modifiedDate = new \DateTime();
|
||||
|
||||
$reflection = new \ReflectionClass($sut);
|
||||
$property = $reflection->getProperty('modifiedAt');
|
||||
|
||||
@@ -219,7 +219,7 @@ class UserTest extends TestCase
|
||||
|
||||
public function testPasswordRequestedAt(): void
|
||||
{
|
||||
$date = new \DateTimeImmutable('-60 minutes');
|
||||
$date = new \DateTime('-60 minutes');
|
||||
$sut = new User();
|
||||
self::assertFalse($sut->isPasswordRequestNonExpired(3599));
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class WorkingTimeTest extends TestCase
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername('bar');
|
||||
$date = new \DateTimeImmutable();
|
||||
$date = new \DateTime();
|
||||
$sut = new WorkingTime($user, $date);
|
||||
|
||||
self::assertSame($user, $sut->getUser());
|
||||
@@ -36,7 +36,7 @@ class WorkingTimeTest extends TestCase
|
||||
self::assertNull($sut->getApprovedBy());
|
||||
self::assertFalse($sut->isApproved());
|
||||
|
||||
$approvedAt = new \DateTimeImmutable('2023-01-01 12:00:00', new \DateTimeZone('Europe/Vienna'));
|
||||
$approvedAt = new \DateTime('2023-01-01 12:00:00', new \DateTimeZone('Europe/Vienna'));
|
||||
$approvedBy = new User();
|
||||
$approvedBy->setUsername('foo');
|
||||
|
||||
|
||||
@@ -49,6 +49,6 @@ class ReportingServiceTest extends TestCase
|
||||
$sut = $this->getSut(true);
|
||||
$reports = $sut->getAvailableReports(new User());
|
||||
self::assertIsArray($reports);
|
||||
self::assertCount(11, $reports);
|
||||
self::assertCount(12, $reports);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1922,6 +1922,16 @@ parameters:
|
||||
count: 1
|
||||
path: Configuration/SamlConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:getDefaultLoaderSettings\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:getDefaultSettings\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:getSut\\(\\) has parameter \\$loaderSettings with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -1932,6 +1942,61 @@ parameters:
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testCalendarWithLoader\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testCalendarWithoutLoader\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testDefaultWithLoader\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testDefaultWithMixedConfigs\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testDefaultWithoutLoader\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testFormDefaultWithLoader\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testFormDefaultWithoutLoader\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testOffsetUnsetThrowsException\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testTimesheetWithLoader\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testTimesheetWithoutLoader\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\SystemConfigurationTest\\:\\:testUnknownConfigs\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/SystemConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\ConsoleApplicationTest\\:\\:testVersion\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
|
||||
Reference in New Issue
Block a user