Release 2.0.15 (#3970)

This commit is contained in:
Kevin Papst
2023-04-16 00:44:00 +02:00
committed by GitHub
parent ecd8ee85f3
commit 046ed313c9
42 changed files with 796 additions and 935 deletions

View File

@@ -76,6 +76,7 @@ final class TimesheetController extends BaseApiController
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]
#[Rest\QueryParam(name: 'user', requirements: '\d+|all', strict: true, nullable: true, description: "User ID to filter timesheets. Needs permission 'view_other_timesheet', pass 'all' to fetch data for all user (default: current user)")]
#[Rest\QueryParam(name: 'users', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of user IDs to filter, e.g.: users[]=1&users[]=2 (ignored if user=all)')]
#[Rest\QueryParam(name: 'customer', requirements: '\d+', strict: true, nullable: true, description: 'Customer ID to filter timesheets')]
#[Rest\QueryParam(name: 'customers', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of customer IDs to filter, e.g.: customers[]=1&customers[]=2')]
#[Rest\QueryParam(name: 'project', requirements: '\d+', strict: true, nullable: true, description: 'Project ID to filter timesheets')]
@@ -98,24 +99,32 @@ final class TimesheetController extends BaseApiController
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
{
$query = new TimesheetQuery(false);
$query->setUser($this->getUser());
$seeAll = false;
if ($this->isGranted('view_other_timesheet')) {
/** @var array<int> $users */
$users = $paramFetcher->get('users');
$userId = $paramFetcher->get('user');
if (\is_string($userId) && $userId !== '') {
if ('all' === $userId) {
$query->setUser(null);
} else {
/** @var User|null $user */
$user = $userRepository->find($userId);
if ($user === null) {
throw $this->createNotFoundException('Unknown user: ' . $userId);
}
$query->setUser($user);
if ('all' === $userId) {
$seeAll = true;
} elseif (\is_string($userId) && $userId !== '') {
$users[] = (int) $userId;
}
if (!$seeAll) {
foreach ($userRepository->findByIds($users) as $user) {
$query->addUser($user);
}
}
}
if ($seeAll) {
$query->setUser(null);
} elseif (!$query->hasUsers()) {
$query->setUser($this->getUser());
}
/** @var array<int> $customers */
$customers = $paramFetcher->get('customers');
$customer = $paramFetcher->get('customer');
@@ -476,6 +485,7 @@ final class TimesheetController extends BaseApiController
#[IsGranted('start', 'timesheet')]
#[OA\Response(response: 200, description: 'Restarts a timesheet record for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)]
#[Rest\Get(path: '/{id}/restart', name: 'restart_timesheet_get', requirements: ['id' => '\d+'])]
#[Rest\Patch(path: '/{id}/restart', name: 'restart_timesheet', requirements: ['id' => '\d+'])]
#[ApiSecurity(name: 'apiUser')]
#[ApiSecurity(name: 'apiToken')]

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.0.14';
public const VERSION = '2.0.15';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20014;
public const VERSION_ID = 20015;
/**
* The software name
*/

View File

@@ -672,6 +672,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
$this->id = null;
}
// field will not be set, if it contains a value
$this->modifiedAt = null;
$this->exported = false;
$currentMeta = $this->meta;

View File

@@ -25,7 +25,7 @@ final class ExportToolbarForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']], false, true);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
$this->addActivitySelect($builder, [], true, true, false);

View File

@@ -26,7 +26,7 @@ final class InvoiceToolbarForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']], false, true);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
$this->addActivitySelect($builder, [], true, true, false);

View File

@@ -155,10 +155,10 @@ trait ToolbarFormTrait
]);
}
protected function addDateRange(FormBuilderInterface $builder, array $options, bool $allowEmpty = true, bool $required = false): void
protected function addDateRange(FormBuilderInterface $builder, array $options, bool $allowEmpty = true): void
{
$params = [
'required' => $required,
'required' => !$allowEmpty,
'allow_empty' => $allowEmpty,
];

View File

@@ -134,6 +134,10 @@ final class DateRangeType extends AbstractType
return $range;
}
if ($dates === null) {
throw new TransformationFailedException('Date range missing');
}
if (preg_match($pattern, $dates) !== 1) {
throw new TransformationFailedException('Invalid date range given');
}

View File

@@ -76,9 +76,20 @@ final class TagsSelectType extends AbstractType
$foundTagNames[] = $tag->getName();
}
/** @var array<string> $newNames */
$newNames = array_diff($newNames, $foundTagNames);
foreach ($newNames as $name) {
/** @var array<string> $newNamesCreate */
$newNamesCreate = array_udiff($newNames, $foundTagNames, function (mixed $userTag, mixed $existingTag) {
if (!\is_string($userTag) || !\is_string($existingTag)) {
return -1;
}
if (mb_strtolower($userTag) === mb_strtolower($existingTag)) {
return 0;
}
return strcmp($userTag, $existingTag);
});
foreach ($newNamesCreate as $name) {
$tag = new Tag();
$tag->setName(mb_substr($name, 0, 100));
$this->tagRepository->saveTag($tag);

View File

@@ -77,4 +77,9 @@ final class DefaultInvoiceFormatter implements InvoiceFormatter
$this->locale = $locale;
$this->formatter = null;
}
public function getFormattedAmount(float $amount): string
{
return $this->getFormatter()->amount($amount);
}
}

View File

@@ -34,7 +34,7 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
if ($item->isFixedRate()) {
$appliedRate = $item->getFixedRate();
$amount = $item->getAmount();
$amount = $formatter->getFormattedAmount($item->getAmount());
}
$activity = $item->getActivity();
@@ -83,12 +83,18 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
'entry.date' => $formatter->getFormattedDateTime($begin),
'entry.week' => \intval($begin->format('W')),
'entry.weekyear' => $begin->format('o'),
'entry.user_id' => $user->getId(),
'entry.user_name' => $user->getUserIdentifier(),
'entry.user_title' => $user->getTitle() ?? '',
'entry.user_alias' => $user->getAlias() ?? '',
];
if (null !== $user) {
$values = array_merge($values, [
'entry.user_id' => $user->getId(),
'entry.user_name' => $user->getUserIdentifier(),
'entry.user_title' => $user->getTitle() ?? '',
'entry.user_alias' => $user->getAlias() ?? '',
'entry.user_display' => $user->getDisplayName() ?? '',
]);
}
if (null !== $activity) {
$values = array_merge($values, [
'entry.activity' => $activity->getName(),

View File

@@ -35,7 +35,8 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
'customer.name' => $customer->getName() ?? '',
'customer.contact' => $customer->getContact() ?? '',
'customer.company' => $customer->getCompany() ?? '',
'customer.vat' => $customer->getVatId() ?? '',
'customer.vat' => $customer->getVatId() ?? '', // deprecated since 2.0.15
'customer.vat_id' => $customer->getVatId() ?? '',
'customer.number' => $customer->getNumber() ?? '',
'customer.country' => $customer->getCountry(),
'customer.homepage' => $customer->getHomepage() ?? '',

View File

@@ -22,9 +22,6 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
$subtotal = $model->getCalculator()->getSubtotal();
$formatter = $model->getFormatter();
$begin = $model->getQuery()->getBegin();
$end = $model->getQuery()->getEnd();
$values = [
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()),
@@ -33,6 +30,7 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'invoice.language' => $model->getTemplate()->getLanguage(), // since 1.9
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
'invoice.vat' => $model->getCalculator()->getVat(),
'invoice.tax_hide' => $model->isHideZeroTax() && $tax === 0.00,
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency),
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false),
'invoice.tax_plain' => $tax,
@@ -65,25 +63,66 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'query.end_month' => '', // since 1.9
'query.end_month_number' => '', // since 1.9
'query.end_year' => '', // since 1.9
// since 2.0.15
'user.see_others' => ($model->getQuery()?->getUser() === null),
];
if ($begin !== null) {
$values = array_merge($values, [
'query.day' => $begin->format('d'), // @deprecated - but impossible to delete
'query.month' => $formatter->getFormattedMonthName($begin), // @deprecated - but impossible to delete
'query.month_number' => $begin->format('m'), // @deprecated - but impossible to delete
'query.year' => $begin->format('Y'), // @deprecated - but impossible to delete
'query.begin' => $formatter->getFormattedDateTime($begin),
'query.begin_day' => $begin->format('d'),
'query.begin_month' => $formatter->getFormattedMonthName($begin),
'query.begin_month_number' => $begin->format('m'),
'query.begin_year' => $begin->format('Y'),
'query.end' => $formatter->getFormattedDateTime($end), // since 1.9
'query.end_day' => $end->format('d'), // since 1.9
'query.end_month' => $formatter->getFormattedMonthName($end), // since 1.9
'query.end_month_number' => $end->format('m'), // since 1.9
'query.end_year' => $end->format('Y'), // since 1.9
]);
$query = $model->getQuery();
if ($query !== null) {
$begin = $query->getBegin();
$end = $query->getEnd();
if ($begin !== null) {
$values = array_merge($values, [
'query.day' => $begin->format('d'),
// @deprecated - but impossible to delete
'query.month' => $formatter->getFormattedMonthName($begin),
// @deprecated - but impossible to delete
'query.month_number' => $begin->format('m'),
// @deprecated - but impossible to delete
'query.year' => $begin->format('Y'),
// @deprecated - but impossible to delete
'query.begin' => $formatter->getFormattedDateTime($begin),
'query.begin_day' => $begin->format('d'),
'query.begin_month' => $formatter->getFormattedMonthName($begin),
'query.begin_month_number' => $begin->format('m'),
'query.begin_year' => $begin->format('Y'),
'query.end' => $formatter->getFormattedDateTime($end),
// since 1.9
'query.end_day' => $end->format('d'),
// since 1.9
'query.end_month' => $formatter->getFormattedMonthName($end),
// since 1.9
'query.end_month_number' => $end->format('m'),
// since 1.9
'query.end_year' => $end->format('Y'),
// since 1.9
]);
}
// since 2.0.15
$activity = $query->getActivity();
if ($activity !== null) {
$prefix = 'query.activity.';
$values = array_merge($values, [
$prefix . 'name' => $activity->getName() ?? '',
$prefix . 'comment' => $activity->getComment() ?? '',
]);
}
// since 2.0.15
$project = $query->getProject();
if ($project !== null) {
$prefix = 'query.project.';
$values = array_merge($values, [
$prefix . 'name' => $project->getName() ?? '',
$prefix . 'comment' => $project->getComment() ?? '',
$prefix . 'order_number' => $project->getOrderNumber(),
]);
}
}
$entries = $model->getEntries();

View File

@@ -9,7 +9,6 @@
namespace App\Invoice\Hydrator;
use App\Entity\UserPreference;
use App\Invoice\InvoiceModel;
use App\Invoice\InvoiceModelHydrator;
@@ -28,9 +27,9 @@ final class InvoiceModelUserHydrator implements InvoiceModelHydrator
'user.email' => $user->getEmail(),
'user.title' => $user->getTitle() ?? '',
'user.alias' => $user->getAlias() ?? '',
'user.display' => $user->getDisplayName() ?? '',
];
/** @var UserPreference $metaField */
foreach ($user->getPreferences() as $metaField) {
$values = array_merge($values, [
'user.meta.' . $metaField->getName() => $metaField->getValue(),

View File

@@ -24,6 +24,8 @@ interface InvoiceFormatter
public function getFormattedTime(DateTime $date): string;
public function getFormattedAmount(float $amount): string;
public function getFormattedMoney(float $amount, ?string $currency, bool $withCurrency = true): string;
public function getFormattedMonthName(DateTime $date): string;

View File

@@ -278,7 +278,7 @@ final class ServiceInvoice
{
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
if (null === $document) {
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
throw new \Exception('Please adjust your invoice template, the renderer is invalid: ' . $model->getTemplate()->getRenderer());
}
foreach ($this->getRenderer() as $renderer) {
@@ -426,7 +426,7 @@ final class ServiceInvoice
$calculator = $this->getCalculatorByName($template->getCalculator());
if (null === $calculator) {
throw new \Exception('Please adjust your invoice template, the invoice calculator is invalid: ' . $template->getCalculator());
throw new \Exception('Please adjust your invoice template, the sum calculator is invalid: ' . $template->getCalculator());
}
$model->setCalculator($calculator);

View File

@@ -93,6 +93,14 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface
return array_values($this->users);
}
/**
* Check if there is one or more users in the query
*/
public function hasUsers(): bool
{
return $this->timesheetUser !== null || \count($this->users) > 0;
}
/**
* Limit the data exclusively to the user.
*/

View File

@@ -66,28 +66,6 @@ class TagRepository extends EntityRepository
return $this->findOneBy(['name' => $tagName]);
}
/**
* Find ids of the given tagNames separated by comma
* @param string $tagNames
* @return array
*/
public function findIdsByTagNameList(string $tagNames): array
{
$qb = $this
->createQueryBuilder('t')
->select('t.id');
$list = array_filter(array_unique(array_map('trim', explode(',', $tagNames))));
$cnt = 0;
foreach ($list as $listElem) {
$qb
->orWhere('t.name like :elem' . $cnt)
->setParameter('elem' . $cnt, '%' . $listElem . '%');
$cnt++;
}
return array_column($qb->getQuery()->getScalarResult(), 'id');
}
/**
* Find all tag names in an alphabetical order
*