Release 1.20.3 (#3340)

* fix edit timesheet link
* fix drag & drop creates record for wrong user
* fix search with multiple bindings
* hide user switcher in calendar if there is only one user to choose
* make quick entry responsive for mobile-only users
* mark invoices as exported by default
* fix serialization deprecation warning
* fix duration calculation for fixed rate entries
* updated composer packages
* fix checkbox for horizontal forms
* support pdfContext for PDF invoice templates
This commit is contained in:
Kevin Papst
2022-06-11 13:25:50 +02:00
committed by GitHub
parent b46fdcefad
commit f8cb8900d6
19 changed files with 412 additions and 361 deletions

View File

@@ -14,6 +14,13 @@ form.form-narrow {
}
}
/* Mark as exported checkbox in export and invoice create screen */
.form-horizontal {
.checkbox {
min-height: 20px;
}
}
/* bootstrap3 hack because the filter look plain ugly without it (see report: project month) */
.checkbox-menu li label {
display: block;

View File

@@ -25,6 +25,7 @@
"doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.8",
"doctrine/persistence": "^2",
"erusev/parsedown": "^1.6",
"friendsofsymfony/rest-bundle": "^3.0",
"gedmo/doctrine-extensions": "^3.6",

630
composer.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
"build/app.0cc41cd4.js"
],
"css": [
"build/app.7beab68a.css"
"build/app.4cf06f88.css"
]
},
"invoice": {

View File

@@ -1,5 +1,5 @@
{
"build/app.css": "build/app.7beab68a.css",
"build/app.css": "build/app.4cf06f88.css",
"build/app.js": "build/app.0cc41cd4.js",
"build/invoice.css": "build/invoice.ccdecd42.css",
"build/invoice.js": "build/invoice.19f36eca.js",

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '1.20.2';
public const VERSION = '1.20.3';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 12002;
public const VERSION_ID = 12003;
/**
* The current release status, either "stable" or "dev"
*/

View File

@@ -60,6 +60,14 @@ class CalendarController extends AbstractController
$profile = $values['user'];
}
}
$form = $form->createView();
// hide if the current user is the only available one
if (\count($form->offsetGet('user')->vars['choices']) < 2) {
$form = null;
$profile = $this->getUser();
}
}
$mode = $this->service->getActiveMode();
@@ -80,7 +88,7 @@ class CalendarController extends AbstractController
}
return $this->render('calendar/user.html.twig', [
'form' => ($form === null ? null : $form->createView()),
'form' => $form,
'user' => $profile,
'config' => $config,
'dragAndDrop' => $dragAndDrop,

View File

@@ -1046,6 +1046,17 @@ class User implements UserInterface, EquatableInterface, \Serializable
return true;
}
public function __serialize(): array
{
return [
'id' => $this->id,
'username' => $this->username,
'enabled' => $this->enabled,
'email' => $this->email,
'password' => $this->password,
];
}
/**
* {@inheritdoc}
*/
@@ -1060,6 +1071,18 @@ class User implements UserInterface, EquatableInterface, \Serializable
]);
}
public function __unserialize(array $data): void
{
if (!\array_key_exists('id', $data)) {
return;
}
$this->id = $data['id'];
$this->username = $data['username'];
$this->enabled = $data['enabled'];
$this->email = $data['email'];
$this->password = $data['password'];
}
/**
* {@inheritdoc}
*/

View File

@@ -110,7 +110,7 @@ abstract class AbstractCalculator
{
$time = 0;
foreach ($this->model->getEntries() as $entry) {
if (null === $entry->getFixedRate() && null !== $entry->getDuration()) {
if (null !== $entry->getDuration()) {
$time += $entry->getDuration();
}
}

View File

@@ -32,7 +32,7 @@ abstract class AbstractTwigRenderer implements RendererInterface
$this->twig = $twig;
}
protected function renderTwigTemplate(InvoiceDocument $document, InvoiceModel $model): string
protected function renderTwigTemplate(InvoiceDocument $document, InvoiceModel $model, array $options = []): string
{
$language = $model->getTemplate()->getLanguage();
$formatLocale = $model->getFormatter()->getLocale();
@@ -42,14 +42,14 @@ abstract class AbstractTwigRenderer implements RendererInterface
$entries[] = $model->itemToArray($entry);
}
$options = [
$options = array_merge([
// model should not be used in the future, but we can likely not remove it
'model' => $model,
// new since 1.16.7 - templates should only use the pre-generated values
'invoice' => $model->toArray(),
// new since 1.19.5 - templates should only use the pre-generated values
'entries' => $entries
];
], $options);
return $this->renderTwigTemplateWithLanguage($this->twig, $template, $options, $language, $formatLocale);
}

View File

@@ -10,6 +10,7 @@
namespace App\Invoice\Renderer;
use App\Entity\InvoiceDocument;
use App\Export\ExportContext;
use App\Invoice\InvoiceFilename;
use App\Invoice\InvoiceModel;
use App\Utils\HtmlToPdfConverter;
@@ -37,16 +38,23 @@ final class PdfRenderer extends AbstractTwigRenderer
public function render(InvoiceDocument $document, InvoiceModel $model): Response
{
$content = $this->renderTwigTemplate($document, $model);
$filename = new InvoiceFilename($model);
$content = $this->converter->convertToPdf($content, [
'setAutoTopMargin' => 'pad',
'setAutoBottomMargin' => 'pad',
'margin_top' => 12,
'margin_bottom' => 8,
]);
$context = new ExportContext();
$context->setOption('filename', $filename->getFilename());
$context->setOption('setAutoTopMargin', 'pad');
$context->setOption('setAutoBottomMargin', 'pad');
$context->setOption('margin_top', '12');
$context->setOption('margin_bottom', '8');
$filename = (string) new InvoiceFilename($model);
$content = $this->renderTwigTemplate($document, $model, ['pdfContext' => $context]);
$content = $this->converter->convertToPdf($content, $context->getOptions());
$filename = $context->getOption('filename');
if (empty($filename)) {
$filename = new InvoiceFilename($model);
$filename = $filename->getFilename();
}
$response = new Response($content);

View File

@@ -23,7 +23,7 @@ class InvoiceQuery extends TimesheetQuery
/**
* @var bool
*/
private $markAsExported = false;
private $markAsExported = true;
public function __construct()
{
@@ -33,7 +33,7 @@ class InvoiceQuery extends TimesheetQuery
'exported' => InvoiceQuery::STATE_NOT_EXPORTED,
'state' => self::STATE_STOPPED,
'billable' => true,
'markAsExported' => false,
'markAsExported' => true,
]);
}

View File

@@ -59,36 +59,43 @@ trait RepositorySearchTrait
$searchAnd = $qb->expr()->andX();
if ($this->supportsMetaFields()) {
$i = 0;
$a = 0;
$c = 0;
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
$and = $qb->expr()->andX();
/** @var literal-string $alias */
$alias = 'meta' . $a++;
$paramName = 'metaName' . $i++;
$paramValue = 'metaValue' . $c++;
if ($metaValue === '*') {
$qb->leftJoin($rootAlias . '.meta', 'meta');
$and->add($qb->expr()->eq('meta.name', ':metaName'));
$qb->setParameter('metaName', $metaName);
$and->add($qb->expr()->isNotNull('meta.value'));
$qb->leftJoin($rootAlias . '.meta', $alias);
$and->add($qb->expr()->eq($alias . '.name', ':' . $paramName));
$qb->setParameter($paramName, $metaName);
$and->add($qb->expr()->isNotNull($alias . '.value'));
} elseif ($metaValue === '~') {
$and->add(
sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias)
);
} elseif ($metaValue === '' || $metaValue === null) {
$qb->leftJoin($rootAlias . '.meta', 'meta');
$qb->leftJoin($rootAlias . '.meta', $alias);
$and->add(
$qb->expr()->orX(
$qb->expr()->andX(
$qb->expr()->eq('meta.name', ':metaName'),
$qb->expr()->isNull('meta.value')
$qb->expr()->eq($alias . '.name', ':' . $paramName),
$qb->expr()->isNull($alias . '.value')
),
sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias)
)
);
$qb->setParameter('metaName', $metaName);
$qb->setParameter($paramName, $metaName);
} else {
$qb->leftJoin($rootAlias . '.meta', 'meta');
$and->add($qb->expr()->eq('meta.name', ':metaName'));
$and->add($qb->expr()->like('meta.value', ':metaValue'));
$qb->setParameter('metaName', $metaName);
$qb->setParameter('metaValue', '%' . $metaValue . '%');
$qb->leftJoin($rootAlias . '.meta', $alias);
$and->add($qb->expr()->eq($alias . '.name', ':' . $paramName));
$and->add($qb->expr()->like($alias . '.value', ':' . $paramValue));
$qb->setParameter($paramName, $metaName);
$qb->setParameter($paramValue, '%' . $metaValue . '%');
}
$searchAnd->add($and);
@@ -99,16 +106,18 @@ trait RepositorySearchTrait
if ($searchTerm->hasSearchTerm() && \count($fields) > 0) {
$or = $qb->expr()->orX();
$i = 0;
foreach ($fields as $field) {
$param = 'searchTerm' . $i++;
if (stripos($field, '.') === false) {
$field = $rootAlias . '.' . $field;
}
$or->add(
$qb->expr()->like($field, ':searchTerm'),
$qb->expr()->like($field, ':' . $param),
);
$qb->setParameter($param, '%' . $searchTerm->getSearchTerm() . '%');
}
$searchAnd->add($or);
$qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%');
}
if ($searchAnd->count() > 0) {

View File

@@ -69,12 +69,14 @@
{% set calendarSelector = '#timesheet_calendar' %}
{% set createParams = '' %}
{% set createRoute = 'timesheet_create' %}
{% set editRoute = 'timesheet_edit' %}
{% set canDelete = is_granted('delete_own_timesheet') %}
{% set canCreate = is_granted('create_own_timesheet') %}
{% set canEdit = is_granted('edit_own_timesheet') %}
{% if user != app.user %}
{% set createParams = '&user=' ~ user.id %}
{% set createRoute = 'admin_timesheet_create' %}
{% set editRoute = 'admin_timesheet_edit' %}
{% set canDelete = is_granted('delete_other_timesheet') %}
{% set canCreate = is_granted('create_other_timesheet') %}
{% set canEdit = is_granted('edit_other_timesheet') %}
@@ -303,6 +305,7 @@
let source = entry.parentElement;
let plainData = entry.dataset.entry;
let data = JSON.parse(plainData);
data.user = {{ user.id }};
let urlReplacer = JSON.parse(source.dataset.routeReplacer);
let apiUrl = source.dataset.route;
@@ -491,7 +494,7 @@
jsEvent.preventDefault();
return;
}
let editUrl = '{{ path('timesheet_edit', {id: '-XX-'}) }}'.replace('-XX-', eventObj.id);
let editUrl = '{{ path(editRoute, {id: '-XX-'}) }}'.replace('-XX-', eventObj.id);
kimai.getPlugin('modal').openUrlInModal(editUrl);
},
{% if not is_punch_mode %}

View File

@@ -16,7 +16,7 @@
{% block box_title %}
{{ form_widget(form.date) }}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}no-padding table-responsive{% endblock %}
{% block box_footer %}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
<button type="button" class="btn btn-success add-item-link" data-collection-prototype="{{ form.rows.vars.id }}" data-collection-holder="ts-collection">

View File

@@ -126,7 +126,7 @@ class PriceInvoiceCalculatorTest extends AbstractCalculatorTest
$this->assertEquals(19, $sut->getVat());
$this->assertEquals('EUR', $model->getCurrency());
$this->assertEquals(2521.12, $sut->getSubtotal());
$this->assertEquals(4800, $sut->getTimeWorked());
$this->assertEquals(6600, $sut->getTimeWorked());
$entries = $sut->getEntries();
self::assertCount(4, $entries);

View File

@@ -266,7 +266,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
$this->assertEquals(19, $sut->getVat());
$this->assertEquals('EUR', $model->getCurrency());
$this->assertEquals(488.38, $sut->getSubtotal());
$this->assertEquals(5400, $sut->getTimeWorked());
$this->assertEquals(5800, $sut->getTimeWorked());
$this->assertEquals(1, \count($sut->getEntries()));
/** @var InvoiceItem $result */

View File

@@ -51,9 +51,9 @@ class InvoiceQueryTest extends TimesheetQueryTest
protected function assertMarkAsExported(InvoiceQuery $sut)
{
self::assertFalse($sut->isMarkAsExported());
$sut->setMarkAsExported(true);
self::assertTrue($sut->isMarkAsExported());
$sut->setMarkAsExported(false);
self::assertFalse($sut->isMarkAsExported());
}
}