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:
@@ -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;
|
||||
|
||||
@@ -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
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
@@ -6,7 +6,7 @@
|
||||
"build/app.0cc41cd4.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.7beab68a.css"
|
||||
"build/app.4cf06f88.css"
|
||||
]
|
||||
},
|
||||
"invoice": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user