Release 2.0.15 (#3970)
This commit is contained in:
@@ -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')]
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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() ?? '',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user