Release 2.41 (#5653)
This commit is contained in:
@@ -161,7 +161,7 @@ final class ActivityController extends BaseApiController
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$this->repository->saveActivity($activity);
|
||||
$this->activityService->saveActivity($activity);
|
||||
|
||||
$view = new View($activity, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -203,7 +203,7 @@ final class ActivityController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$this->repository->saveActivity($activity);
|
||||
$this->activityService->saveActivity($activity);
|
||||
|
||||
$view = new View($activity, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -253,7 +253,7 @@ final class ActivityController extends BaseApiController
|
||||
|
||||
$meta->setValue($value);
|
||||
|
||||
$this->repository->saveActivity($activity);
|
||||
$this->activityService->saveActivity($activity);
|
||||
|
||||
$view = new View($activity, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
@@ -12,13 +12,12 @@ namespace App\API;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
abstract class BaseApiController extends AbstractController
|
||||
{
|
||||
@@ -60,13 +59,19 @@ abstract class BaseApiController extends AbstractController
|
||||
return DateTimeFactory::createByUser($user);
|
||||
}
|
||||
|
||||
protected function prepareQuery(BaseQuery $query, ParamFetcherInterface $paramFetcher): void
|
||||
/**
|
||||
* @template T of BaseQuery
|
||||
* @param T $query
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return T
|
||||
*/
|
||||
protected function prepareQuery(BaseQuery $query, ParamFetcherInterface $paramFetcher): BaseQuery
|
||||
{
|
||||
$query->setIsApiCall(true);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
// there is no function has() in ParamFetcherInterface, so we need to use all() and check for the key
|
||||
$all = $paramFetcher->all(true);
|
||||
$all = $paramFetcher->all();
|
||||
|
||||
if (\array_key_exists('page', $all)) {
|
||||
$page = $all['page'];
|
||||
@@ -78,33 +83,28 @@ abstract class BaseApiController extends AbstractController
|
||||
if (\array_key_exists('size', $all)) {
|
||||
$size = $all['size'];
|
||||
if (is_numeric($size)) {
|
||||
$query->setPageSize((int) $size);
|
||||
$size = (int) $size;
|
||||
if ($size < 1 || $size > 500) {
|
||||
throw new BadRequestHttpException('Size must be between 1 and 500');
|
||||
}
|
||||
$query->setPageSize($size);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('pageSize', $all)) {
|
||||
$size = $all['pageSize'];
|
||||
if (is_numeric($size)) {
|
||||
$query->setPageSize((int) $size);
|
||||
if (\array_key_exists('order', $all)) {
|
||||
$order = $all['order'];
|
||||
if (\is_string($order) && $order !== '') {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createPaginatedView(Pagination $pagination): View
|
||||
{
|
||||
$results = (array) $pagination->getCurrentPageResults();
|
||||
if (\array_key_exists('orderBy', $all)) {
|
||||
$orderBy = $all['orderBy'];
|
||||
if (\is_string($orderBy) && $orderBy !== '') {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
}
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $pagination);
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
protected function addPagination(View $view, Pagination $pagination): void
|
||||
{
|
||||
$view->setHeader('X-Page', (string) $pagination->getCurrentPage());
|
||||
$view->setHeader('X-Total-Count', (string) $pagination->getNbResults());
|
||||
$view->setHeader('X-Total-Pages', (string) $pagination->getNbPages());
|
||||
$view->setHeader('X-Per-Page', (string) $pagination->getMaxPerPage());
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ final class CustomerController extends BaseApiController
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$this->repository->saveCustomer($customer);
|
||||
$this->customerService->saveCustomer($customer);
|
||||
|
||||
$view = new View($customer, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -178,7 +178,7 @@ final class CustomerController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$this->repository->saveCustomer($customer);
|
||||
$this->customerService->saveCustomer($customer);
|
||||
|
||||
$view = new View($customer, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -228,7 +228,7 @@ final class CustomerController extends BaseApiController
|
||||
|
||||
$meta->setValue($value);
|
||||
|
||||
$this->repository->saveCustomer($customer);
|
||||
$this->customerService->saveCustomer($customer);
|
||||
|
||||
$view = new View($customer, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
@@ -80,7 +80,7 @@ final class InvoiceController extends BaseApiController
|
||||
}
|
||||
|
||||
$data = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($data);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
@@ -82,7 +82,7 @@ final class TimesheetController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'activity', requirements: '\d+', strict: true, nullable: true, description: 'Activity ID to filter timesheets')]
|
||||
#[Rest\QueryParam(name: 'activities', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of activity IDs to filter, e.g.: activities[]=1&activities[]=2')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50, max: 500)')]
|
||||
#[Rest\QueryParam(name: 'tags', map: true, strict: true, nullable: true, default: [], description: 'List of tag names, e.g. tags[]=bar&tags[]=foo')]
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|begin|end|rate', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)')]
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: DESC)')]
|
||||
@@ -97,7 +97,7 @@ final class TimesheetController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||
{
|
||||
$query = new TimesheetQuery(false);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
$seeAll = false;
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
@@ -169,16 +169,6 @@ final class TimesheetController extends BaseApiController
|
||||
$query->addActivity($activity);
|
||||
}
|
||||
|
||||
$page = $paramFetcher->get('page');
|
||||
if (\is_string($page) && $page !== '') {
|
||||
$query->setPage((int) $page);
|
||||
}
|
||||
|
||||
$size = $paramFetcher->get('size');
|
||||
if (\is_string($size) && $size !== '') {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
|
||||
/** @var array<string> $tags */
|
||||
$tags = $paramFetcher->get('tags');
|
||||
if (\is_array($tags) && \count($tags) > 0) {
|
||||
@@ -191,16 +181,6 @@ final class TimesheetController extends BaseApiController
|
||||
}
|
||||
}
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
if (\is_string($order) && $order !== '') {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
$orderBy = $paramFetcher->get('orderBy');
|
||||
if (\is_string($orderBy) && $orderBy !== '') {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
$factory = $this->getDateTimeFactory();
|
||||
|
||||
$begin = $paramFetcher->get('begin');
|
||||
@@ -252,12 +232,9 @@ final class TimesheetController extends BaseApiController
|
||||
$query->setModifiedAfter($factory->createDateTime($modifiedAfter));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getPagerfantaForQuery($query);
|
||||
$results = (array) $data->getCurrentPageResults();
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $data);
|
||||
$view = new View($data, 200);
|
||||
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
|
||||
78
src/API/ViewHandler.php
Normal file
78
src/API/ViewHandler.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?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\API;
|
||||
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\View\ConfigurableViewHandlerInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ViewHandler implements ConfigurableViewHandlerInterface
|
||||
{
|
||||
public function __construct(private readonly ConfigurableViewHandlerInterface $baseViewHandler)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|string $groups
|
||||
*/
|
||||
public function setExclusionStrategyGroups($groups): void
|
||||
{
|
||||
$this->baseViewHandler->setExclusionStrategyGroups($groups);
|
||||
}
|
||||
|
||||
public function setExclusionStrategyVersion(string $version): void
|
||||
{
|
||||
$this->baseViewHandler->setExclusionStrategyVersion($version);
|
||||
}
|
||||
|
||||
public function setSerializeNullStrategy(bool $isEnabled): void
|
||||
{
|
||||
$this->baseViewHandler->setSerializeNullStrategy($isEnabled);
|
||||
}
|
||||
|
||||
public function supports(string $format): bool
|
||||
{
|
||||
return $this->baseViewHandler->supports($format);
|
||||
}
|
||||
|
||||
public function registerHandler(string $format, callable $callable): void
|
||||
{
|
||||
$this->baseViewHandler->registerHandler($format, $callable);
|
||||
}
|
||||
|
||||
public function handle(View $view, ?Request $request = null): Response
|
||||
{
|
||||
$data = $view->getData();
|
||||
|
||||
if ($data instanceof Pagination) {
|
||||
$results = (array) $data->getCurrentPageResults();
|
||||
$view->setData($results);
|
||||
|
||||
$view->setHeader('X-Page', (string) $data->getCurrentPage());
|
||||
$view->setHeader('X-Total-Count', (string) $data->getNbResults());
|
||||
$view->setHeader('X-Total-Pages', (string) $data->getNbPages());
|
||||
$view->setHeader('X-Per-Page', (string) $data->getMaxPerPage());
|
||||
}
|
||||
|
||||
return $this->baseViewHandler->handle($view, $request);
|
||||
}
|
||||
|
||||
public function createRedirectResponse(View $view, string $location, string $format): Response
|
||||
{
|
||||
return $this->baseViewHandler->createRedirectResponse($view, $location, $format);
|
||||
}
|
||||
|
||||
public function createResponse(View $view, Request $request, string $format): Response
|
||||
{
|
||||
return $this->baseViewHandler->createResponse($view, $request, $format);
|
||||
}
|
||||
}
|
||||
@@ -277,7 +277,7 @@ final class InvoiceCreateCommand extends Command
|
||||
|
||||
$tpl = $this->getTemplateForCustomer($input, $customer);
|
||||
if (null === $tpl) {
|
||||
$io->warning(\sprintf('Could not find invoice template for project "%s", skipping!', $project->getName()));
|
||||
$io->warning('Could not find invoice template for project, skipping.');
|
||||
continue;
|
||||
}
|
||||
$query->setTemplate($tpl);
|
||||
@@ -295,7 +295,7 @@ final class InvoiceCreateCommand extends Command
|
||||
$invoices[] = $this->serviceInvoice->createInvoice($model, $this->eventDispatcher);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(\sprintf('Failed to create invoice for project "%s" with: %s', $project->getName(), $ex->getMessage()));
|
||||
$io->error(\sprintf('Failed to create invoice for project with: %s', $ex->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ final class InvoiceCreateCommand extends Command
|
||||
|
||||
$tpl = $this->getTemplateForCustomer($input, $customer);
|
||||
if (null === $tpl) {
|
||||
$io->warning(\sprintf('Could not find invoice template for customer "%s", skipping!', $customer->getName()));
|
||||
$io->warning('Could not find invoice template for customer, skipping.');
|
||||
continue;
|
||||
}
|
||||
$query->setTemplate($tpl);
|
||||
@@ -370,7 +370,7 @@ final class InvoiceCreateCommand extends Command
|
||||
$invoices[] = $this->serviceInvoice->createInvoice($model, $this->eventDispatcher);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(\sprintf('Failed to create invoice for customer "%s" with: %s', $customer->getName(), $ex->getMessage()));
|
||||
$io->error(\sprintf('Failed to create invoice for customer with: %s', $ex->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ final class InvoiceCreateCommand extends Command
|
||||
$file = $this->serviceInvoice->getInvoiceFile($invoice);
|
||||
if (null === $file) {
|
||||
$io->warning(
|
||||
\sprintf('Created invoice with ID %s, but file was not found %s', $invoice->getId(), $invoice->getInvoiceFilename())
|
||||
\sprintf('Created invoice with ID %s, but file was not found %s', $invoice->getId() ?? 'unknown', $invoice->getInvoiceFilename() ?? 'unknown')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.40.0';
|
||||
public const VERSION = '2.41.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 24000;
|
||||
public const VERSION_ID = 24100;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,7 @@ use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\Query\VisibilityInterface;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
@@ -146,7 +147,7 @@ final class CustomerController extends AbstractController
|
||||
{
|
||||
$customer = $customerService->createNewCustomer('');
|
||||
|
||||
return $this->renderCustomerForm($customer, $request, true);
|
||||
return $this->renderCustomerForm($customer, $request, true, $customerService);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/permissions', name: 'admin_customer_permissions', methods: ['GET', 'POST'])]
|
||||
@@ -281,7 +282,7 @@ final class CustomerController extends AbstractController
|
||||
$query->setPage($page);
|
||||
$query->setPageSize(5);
|
||||
$query->addCustomer($customer);
|
||||
$query->setShowBoth();
|
||||
$query->setVisibility(VisibilityInterface::SHOW_BOTH);
|
||||
$query->addOrderGroup('visible', ProjectQuery::ORDER_DESC);
|
||||
$query->addOrderGroup('name', ProjectQuery::ORDER_ASC);
|
||||
|
||||
@@ -421,9 +422,9 @@ final class CustomerController extends AbstractController
|
||||
|
||||
#[Route(path: '/{id}/edit', name: 'admin_customer_edit', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('edit', 'customer')]
|
||||
public function editAction(Customer $customer, Request $request): Response
|
||||
public function editAction(Customer $customer, Request $request, CustomerService $customerService): Response
|
||||
{
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
return $this->renderCustomerForm($customer, $request, false, $customerService);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/delete', name: 'admin_customer_delete', methods: ['GET', 'POST'])]
|
||||
@@ -496,7 +497,7 @@ final class CustomerController extends AbstractController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function renderCustomerForm(Customer $customer, Request $request, bool $create = false): Response
|
||||
private function renderCustomerForm(Customer $customer, Request $request, bool $create, CustomerService $customerService): Response
|
||||
{
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
@@ -504,7 +505,7 @@ final class CustomerController extends AbstractController
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveCustomer($customer);
|
||||
$customerService->saveCustomer($customer);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($create) {
|
||||
|
||||
@@ -321,7 +321,7 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
if (null === $file) {
|
||||
throw $this->createNotFoundException(
|
||||
\sprintf('Invoice file "%s" could not be found for invoice ID "%s"', $invoice->getInvoiceFilename(), $invoice->getId())
|
||||
\sprintf('Invoice file could not be found for invoice ID "%s"', $invoice->getId())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\Query\VisibilityInterface;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\Context;
|
||||
use App\Utils\DataTable;
|
||||
@@ -313,7 +314,7 @@ final class ProjectController extends AbstractController
|
||||
$query->setPageSize(5);
|
||||
$query->addProject($project);
|
||||
$query->setExcludeGlobals(true);
|
||||
$query->setShowBoth();
|
||||
$query->setVisibility(VisibilityInterface::SHOW_BOTH);
|
||||
$query->addOrderGroup('visible', ActivityQuery::ORDER_DESC);
|
||||
$query->addOrderGroup('name', ActivityQuery::ORDER_ASC);
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ final class QuickEntryController extends AbstractController
|
||||
$endWeek = $factory->getEndOfWeek($begin);
|
||||
|
||||
$tmpDay = clone $startWeek;
|
||||
/** @var array<string, array{day: \DateTime}> $week */
|
||||
$week = [];
|
||||
while ($tmpDay < $endWeek) {
|
||||
$nextDay = clone $tmpDay;
|
||||
@@ -125,7 +126,7 @@ final class QuickEntryController extends AbstractController
|
||||
if ($amount > 0) {
|
||||
$takeOverWeeks = $this->configuration->find('quick_entry.recent_activity_weeks');
|
||||
$startFrom = null;
|
||||
if ($takeOverWeeks !== null && \intval($takeOverWeeks) > 0) {
|
||||
if (is_numeric($takeOverWeeks) && \intval($takeOverWeeks) > 0) {
|
||||
$startFrom = clone $startWeek;
|
||||
$startFrom->modify(\sprintf('-%s weeks', (string) $takeOverWeeks));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -94,6 +95,10 @@ final class TeamController extends AbstractController
|
||||
{
|
||||
$newTeam = clone $team;
|
||||
|
||||
if ($team->getName() === null) {
|
||||
throw new BadRequestHttpException('Team with empty name cannot be duplicated');
|
||||
}
|
||||
|
||||
$i = 1;
|
||||
do {
|
||||
$newName = \sprintf('%s (%s)', $team->getName(), $i++);
|
||||
|
||||
@@ -153,18 +153,18 @@ final class TimesheetFixtures extends Fixture implements FixtureGroupInterface
|
||||
|
||||
$qb = $manager->getRepository($class)->createQueryBuilder('entity');
|
||||
|
||||
/** @var array<int, T> $all */
|
||||
$all = $qb->where($qb->expr()->in('entity.id', $ids))->setMaxResults($amount)->getQuery()->getResult();
|
||||
/** @var array<int, T> $result */
|
||||
$result = $qb->where($qb->expr()->in('entity.id', $ids))->setMaxResults($amount)->getQuery()->getResult();
|
||||
|
||||
if (\count($all) === 0) {
|
||||
throw new \Exception('Need users to setup teams');
|
||||
if (\count($result) === 0) {
|
||||
throw new \Exception('Could not find any entity: ' . $class);
|
||||
}
|
||||
|
||||
return $all;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, Tag>
|
||||
* @return non-empty-array<int|string, Tag>
|
||||
*/
|
||||
private function getAllTags(ObjectManager $manager): array
|
||||
{
|
||||
|
||||
@@ -236,6 +236,8 @@ final class ColumnConverter
|
||||
$columns[$column] = (new Column('vat_id', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getVatId());
|
||||
} elseif ($column === 'project.order_number') {
|
||||
$columns[$column] = (new Column('orderNumber', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getOrderNumber());
|
||||
} elseif ($column === 'id') {
|
||||
$columns[$column] = (new Column('id', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getId());
|
||||
} elseif (str_starts_with($column, 'timesheet.meta.') && \array_key_exists($column, $timesheetMeta)) {
|
||||
$columns[$column] = $timesheetMeta[$column];
|
||||
} elseif (str_starts_with($column, 'customer.meta.') && \array_key_exists($column, $customerMeta)) {
|
||||
|
||||
@@ -49,6 +49,7 @@ final class DurationType extends AbstractType
|
||||
$class .= ' ' . $view->vars['attr']['class'];
|
||||
}
|
||||
$view->vars['attr']['class'] = $class;
|
||||
$view->vars['attr']['autocomplete'] = 'off';
|
||||
$view->vars['toggle'] = $options['toggle'];
|
||||
|
||||
if ($options['preset_hours'] !== null && $options['preset_minutes'] !== null) {
|
||||
|
||||
@@ -49,6 +49,7 @@ final class ExportColumnsType extends AbstractType
|
||||
{
|
||||
$columns = [
|
||||
'timesheet' => [
|
||||
'id' => 'id',
|
||||
'date' => 'date',
|
||||
'begin' => 'begin',
|
||||
'end' => 'end',
|
||||
|
||||
@@ -12,15 +12,48 @@ namespace App\Invoice\Calculator;
|
||||
use App\Invoice\InvoiceItem;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\TaxRow;
|
||||
use App\Timesheet\Util;
|
||||
|
||||
abstract class AbstractCalculator
|
||||
{
|
||||
protected InvoiceModel $model;
|
||||
/**
|
||||
* @var InvoiceItem[]
|
||||
*/
|
||||
private array $cached = [];
|
||||
|
||||
/**
|
||||
* TODO make this method abstract in 3.0
|
||||
*
|
||||
* @return InvoiceItem[]
|
||||
*/
|
||||
abstract public function getEntries(): array;
|
||||
protected function calculateEntries(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO make this method final in 3.0
|
||||
*
|
||||
* @return InvoiceItem[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
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()));
|
||||
// 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()));
|
||||
}
|
||||
|
||||
$this->cached[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<InvoiceItem> $items
|
||||
@@ -45,11 +78,12 @@ abstract class AbstractCalculator
|
||||
public function getSubtotal(): float
|
||||
{
|
||||
$amount = 0.00;
|
||||
foreach ($this->model->getEntries() as $entry) {
|
||||
// using the entries and not the raw data, so we make sure to use the same base for everything
|
||||
foreach ($this->getEntries() as $entry) {
|
||||
$amount += $entry->getRate();
|
||||
}
|
||||
|
||||
return round($amount, 2);
|
||||
return round($amount, 2, PHP_ROUND_HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,12 +114,12 @@ abstract class AbstractCalculator
|
||||
$tax += $row->getAmount();
|
||||
}
|
||||
|
||||
return round($tax, 2);
|
||||
return round($tax, 2, PHP_ROUND_HALF_UP);
|
||||
}
|
||||
|
||||
public function getTotal(): float
|
||||
{
|
||||
return $this->getSubtotal() + $this->getTax();
|
||||
return round($this->getSubtotal() + $this->getTax(), 2, PHP_ROUND_HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +128,8 @@ abstract class AbstractCalculator
|
||||
public function getTimeWorked(): int
|
||||
{
|
||||
$time = 0;
|
||||
foreach ($this->model->getEntries() as $entry) {
|
||||
// using the entries and not the raw data, so we make sure to use the same base for everything
|
||||
foreach ($this->getEntries() as $entry) {
|
||||
if (null !== $entry->getDuration()) {
|
||||
$time += $entry->getDuration();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
|
||||
$invoiceItem->setAmount($invoiceItem->getAmount() + $amount);
|
||||
$invoiceItem->setUser($entry->getUser());
|
||||
$invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate());
|
||||
$invoiceItem->setInternalRate($invoiceItem->getInternalRate() + ($entry->getInternalRate() ?? 0.00));
|
||||
$invoiceItem->setInternalRate($invoiceItem->getInternalRate() + ($entry->getInternalRate() ?? 0.00)); // @phpstan-ignore method.deprecated,method.deprecated
|
||||
$invoiceItem->setDuration($duration);
|
||||
|
||||
if (null !== $entry->getFixedRate()) {
|
||||
|
||||
@@ -47,16 +47,16 @@ abstract class AbstractSumInvoiceCalculator extends AbstractMergedCalculator imp
|
||||
$prefix = $this->calculateSumIdentifier($entry);
|
||||
|
||||
if (null !== $entry->getFixedRate()) {
|
||||
return $prefix . '_fixed_' . (string) $entry->getFixedRate();
|
||||
return $prefix . '_fixed_' . $entry->getFixedRate();
|
||||
}
|
||||
|
||||
return $prefix . '_hourly_' . (string) $entry->getHourlyRate();
|
||||
return $prefix . '_hourly_' . ($entry->getHourlyRate() ?? '__NULL__');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InvoiceItem[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
protected function calculateEntries(): array
|
||||
{
|
||||
$entries = $this->model->getEntries();
|
||||
if (empty($entries)) {
|
||||
|
||||
@@ -23,7 +23,7 @@ final class DefaultCalculator extends AbstractMergedCalculator implements Calcul
|
||||
/**
|
||||
* @return InvoiceItem[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
protected function calculateEntries(): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ final class PriceInvoiceCalculator extends AbstractSumInvoiceCalculator implemen
|
||||
return ['fixed_' . $invoiceItem->getFixedRate()];
|
||||
}
|
||||
|
||||
return ['hourly_' . $invoiceItem->getHourlyRate()];
|
||||
return ['hourly_' . ($invoiceItem->getHourlyRate() ?? '__NULL__')];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
|
||||
@@ -21,7 +21,7 @@ final class ShortInvoiceCalculator extends AbstractMergedCalculator implements C
|
||||
/**
|
||||
* @return InvoiceItem[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
protected function calculateEntries(): array
|
||||
{
|
||||
$entries = $this->model->getEntries();
|
||||
if (empty($entries)) {
|
||||
|
||||
@@ -29,7 +29,7 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
||||
$formatter = $this->model->getFormatter();
|
||||
|
||||
$rate = $item->getRate();
|
||||
$internalRate = $item->getInternalRate();
|
||||
$internalRate = $item->getInternalRate(); // @phpstan-ignore method.deprecated
|
||||
$appliedRate = $item->getHourlyRate();
|
||||
$amount = $formatter->getFormattedDecimalDuration($item->getDuration());
|
||||
$description = $item->getDescription();
|
||||
@@ -63,9 +63,9 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
||||
'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency),
|
||||
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, $currency, false),
|
||||
'entry.rate_plain' => $appliedRate,
|
||||
'entry.rate_internal' => $formatter->getFormattedMoney($internalRate, $currency),
|
||||
'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, $currency, false),
|
||||
'entry.rate_internal_plain' => $internalRate,
|
||||
'entry.rate_internal' => $formatter->getFormattedMoney($internalRate, $currency), // @deprecated since 2.41
|
||||
'entry.rate_internal_nc' => $formatter->getFormattedMoney($internalRate, $currency, false), // @deprecated since 2.41
|
||||
'entry.rate_internal_plain' => $internalRate, // @deprecated since 2.41
|
||||
'entry.rate_fixed' => ($item->isFixedRate() ? $item->getFixedRate() : null),
|
||||
'entry.total' => $formatter->getFormattedMoney($rate, $currency),
|
||||
'entry.total_nc' => $formatter->getFormattedMoney($rate, $currency, false),
|
||||
|
||||
@@ -19,6 +19,9 @@ final class InvoiceItem
|
||||
private ?float $fixedRate = null;
|
||||
private ?float $hourlyRate = null;
|
||||
private float $rate = 0.00;
|
||||
/**
|
||||
* @deprecated since 2.41 - internal rate is not needed in invoices
|
||||
*/
|
||||
private float $rateInternal = 0.00;
|
||||
private float $amount = 0.00;
|
||||
private ?string $description = null;
|
||||
@@ -85,9 +88,14 @@ final class InvoiceItem
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAppliedRate(): float
|
||||
{
|
||||
return $this->fixedRate ?? $this->hourlyRate ?? 0.00;
|
||||
}
|
||||
|
||||
public function isFixedRate(): bool
|
||||
{
|
||||
return null !== $this->getFixedRate();
|
||||
return $this->fixedRate !== null;
|
||||
}
|
||||
|
||||
public function getFixedRate(): ?float
|
||||
@@ -126,11 +134,17 @@ final class InvoiceItem
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.41 - internal rate is not needed in invoices
|
||||
*/
|
||||
public function getInternalRate(): float
|
||||
{
|
||||
return $this->rateInternal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.41 - internal rate is not needed in invoices
|
||||
*/
|
||||
public function setInternalRate(float $rateInternal): InvoiceItem
|
||||
{
|
||||
$this->rateInternal = $rateInternal;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Project;
|
||||
|
||||
use App\Activity\ActivityService;
|
||||
use App\Entity\Project;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
@@ -21,7 +22,8 @@ final class ProjectDuplicationService
|
||||
private readonly ProjectService $projectService,
|
||||
private readonly ActivityRepository $activityRepository,
|
||||
private readonly ProjectRateRepository $projectRateRepository,
|
||||
private readonly ActivityRateRepository $activityRateRepository
|
||||
private readonly ActivityRateRepository $activityRateRepository,
|
||||
private readonly ActivityService $activityService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -68,7 +70,7 @@ final class ProjectDuplicationService
|
||||
$newActivity->setMetaField($newMetaField);
|
||||
}
|
||||
|
||||
$this->activityRepository->saveActivity($newActivity);
|
||||
$this->activityService->saveActivity($newActivity);
|
||||
|
||||
foreach ($this->activityRateRepository->getRatesForActivity($activity) as $rate) {
|
||||
$newRate = clone $rate;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
class TagQuery extends BaseQuery
|
||||
class TagQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
use VisibilityTrait;
|
||||
|
||||
|
||||
@@ -20,10 +20,9 @@ trait VisibilityTrait
|
||||
|
||||
public function setVisibility(int $visibility): void
|
||||
{
|
||||
if (!\in_array($visibility, VisibilityInterface::ALLOWED_VISIBILITY_STATES, true)) {
|
||||
throw new \InvalidArgumentException('Unknown visibility given');
|
||||
if (\in_array($visibility, VisibilityInterface::ALLOWED_VISIBILITY_STATES, true)) {
|
||||
$this->visibility = $visibility;
|
||||
}
|
||||
$this->visibility = $visibility;
|
||||
}
|
||||
|
||||
public function isShowHidden(): bool
|
||||
@@ -36,6 +35,9 @@ trait VisibilityTrait
|
||||
return $this->visibility === VisibilityInterface::SHOW_VISIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.41
|
||||
*/
|
||||
public function setShowBoth(): void
|
||||
{
|
||||
$this->setVisibility(VisibilityInterface::SHOW_BOTH);
|
||||
|
||||
@@ -43,8 +43,10 @@ final class SearchHelper
|
||||
|
||||
$rootAlias = $aliases[0];
|
||||
$searchAnd = $qb->expr()->andX();
|
||||
$metaFieldClass = $this->configuration->getMetaFieldClass();
|
||||
$metaFieldName = $this->configuration->getMetaFieldName();
|
||||
|
||||
if ($this->supportsMetaFields()) {
|
||||
if ($metaFieldClass !== null && $metaFieldName !== null && $this->supportsMetaFields()) {
|
||||
$metaFieldRef = $rootAlias . '.' . $this->configuration->getEntityFieldName();
|
||||
$i = 0;
|
||||
$c = 0;
|
||||
@@ -69,7 +71,7 @@ final class SearchHelper
|
||||
$and->add($qb->expr()->isNotNull($field));
|
||||
} elseif ($metaValue === '~') {
|
||||
$and->add(
|
||||
\sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id AND %s.name = :%s)', $subqueryName, $this->configuration->getMetaFieldClass(), $subqueryName, $subqueryName, $this->configuration->getMetaFieldName(), $rootAlias, $subqueryName, $paramName)
|
||||
\sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id AND %s.name = :%s)', $subqueryName, $metaFieldClass, $subqueryName, $subqueryName, $metaFieldName, $rootAlias, $subqueryName, $paramName)
|
||||
);
|
||||
} elseif ($metaValue === '') {
|
||||
$and->add(
|
||||
@@ -78,7 +80,7 @@ final class SearchHelper
|
||||
$qb->expr()->eq($alias . '.name', ':' . $paramName),
|
||||
$qb->expr()->isNull($field)
|
||||
),
|
||||
\sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id AND %s.name = :%s)', $subqueryName, $this->configuration->getMetaFieldClass(), $subqueryName, $subqueryName, $this->configuration->getMetaFieldName(), $rootAlias, $subqueryName, $paramName)
|
||||
\sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id AND %s.name = :%s)', $subqueryName, $metaFieldClass, $subqueryName, $subqueryName, $metaFieldName, $rootAlias, $subqueryName, $paramName)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -54,7 +54,7 @@ final class SamlProvider
|
||||
} catch (\Exception $ex) {
|
||||
$this->logger->error($ex->getMessage());
|
||||
throw new AuthenticationException(
|
||||
\sprintf('Failed creating or hydrating user "%s": %s', $token->getUserIdentifier(), $ex->getMessage())
|
||||
\sprintf('Failed creating or hydrating user "%s": %s', $token->getUserIdentifier() ?? '*unknown*', $ex->getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,18 @@ final class Util
|
||||
*/
|
||||
public static function calculateRate(float $hourlyRate, int $seconds): float
|
||||
{
|
||||
$rate = $hourlyRate * ($seconds / 3600);
|
||||
$rate = $hourlyRate * round(($seconds / 3600), 2, PHP_ROUND_HALF_UP);
|
||||
|
||||
return round($rate, 4);
|
||||
return round($rate, 2, PHP_ROUND_HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure tha the duration is full compatible with decimal format, stripping away overflowing seconds.
|
||||
*/
|
||||
public static function decimalizeDuration(int $seconds): int
|
||||
{
|
||||
$decimal = round(($seconds / 3600), 2, PHP_ROUND_HALF_UP);
|
||||
|
||||
return (int) round(($decimal * 3600), 0, PHP_ROUND_HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,15 @@ class Parsedown extends \Parsedown
|
||||
$block = parent::blockHeader($Line);
|
||||
|
||||
$text = $block['element']['text'];
|
||||
$id = $this->getIDfromText($text);
|
||||
|
||||
// add id-attribute
|
||||
$block['element']['attributes'] = [
|
||||
'id' => $id
|
||||
];
|
||||
if (\is_string($text) && $text !== '') {
|
||||
$id = $this->getIDfromText($text);
|
||||
|
||||
// add id-attribute
|
||||
$block['element']['attributes'] = [
|
||||
'id' => $id
|
||||
];
|
||||
}
|
||||
|
||||
return $block;
|
||||
}
|
||||
@@ -67,7 +70,7 @@ class Parsedown extends \Parsedown
|
||||
return $text;
|
||||
}
|
||||
|
||||
protected function blockTable($Line, array $Block = null) // @phpstan-ignore missingType.return,missingType.iterableValue,missingType.parameter
|
||||
protected function blockTable($Line, ?array $Block = null) // @phpstan-ignore missingType.return,missingType.iterableValue,missingType.parameter
|
||||
{
|
||||
$Block = parent::blockTable($Line, $Block);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ final class WorkingTimeModeFactory
|
||||
return $this->getMode($user->getWorkContractMode());
|
||||
} catch (\InvalidArgumentException $ex) {
|
||||
$this->logger->error(
|
||||
\sprintf('Unknown mode "%s" requested for user %s', $user->getWorkContractMode(), $user->getId())
|
||||
\sprintf('Unknown mode "%s" requested for user %s', $user->getWorkContractMode(), $user->getUserIdentifier())
|
||||
);
|
||||
|
||||
return new WorkingTimeModeNone(); // @CloudRequired
|
||||
|
||||
Reference in New Issue
Block a user