customize html and pdf exports with twig templates (#1582)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ templates/invoice/renderer/.~lock*
|
|||||||
!var/cache/.gitkeep
|
!var/cache/.gitkeep
|
||||||
|
|
||||||
/var/invoices/*
|
/var/invoices/*
|
||||||
|
/var/export/*
|
||||||
|
|
||||||
/var/packages/*
|
/var/packages/*
|
||||||
!var/packages/.gitkeep
|
!var/packages/.gitkeep
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ class AppExtension extends Extension
|
|||||||
$config['invoice']['documents'] = array_merge($config['invoice']['documents'], $config['invoice']['defaults']);
|
$config['invoice']['documents'] = array_merge($config['invoice']['documents'], $config['invoice']['defaults']);
|
||||||
unset($config['invoice']['defaults']);
|
unset($config['invoice']['defaults']);
|
||||||
|
|
||||||
|
$config['export']['documents'] = array_merge($config['export']['documents'], $config['export']['defaults']);
|
||||||
|
unset($config['export']['defaults']);
|
||||||
|
|
||||||
// safe alternatives to %kernel.project_dir%
|
// safe alternatives to %kernel.project_dir%
|
||||||
$container->setParameter('kimai.data_dir', $config['data_dir']);
|
$container->setParameter('kimai.data_dir', $config['data_dir']);
|
||||||
$container->setParameter('kimai.plugin_dir', $config['plugin_dir']);
|
$container->setParameter('kimai.plugin_dir', $config['plugin_dir']);
|
||||||
@@ -60,6 +63,7 @@ class AppExtension extends Extension
|
|||||||
$container->setParameter('kimai.dashboard', $config['dashboard']);
|
$container->setParameter('kimai.dashboard', $config['dashboard']);
|
||||||
$container->setParameter('kimai.widgets', $config['widgets']);
|
$container->setParameter('kimai.widgets', $config['widgets']);
|
||||||
$container->setParameter('kimai.invoice.documents', $config['invoice']['documents']);
|
$container->setParameter('kimai.invoice.documents', $config['invoice']['documents']);
|
||||||
|
$container->setParameter('kimai.export.documents', $config['export']['documents']);
|
||||||
$container->setParameter('kimai.defaults', $config['defaults']);
|
$container->setParameter('kimai.defaults', $config['defaults']);
|
||||||
|
|
||||||
$this->createPermissionParameter($config['permissions'], $container);
|
$this->createPermissionParameter($config['permissions'], $container);
|
||||||
|
|||||||
@@ -9,10 +9,14 @@
|
|||||||
|
|
||||||
namespace App\DependencyInjection\Compiler;
|
namespace App\DependencyInjection\Compiler;
|
||||||
|
|
||||||
|
use App\Export\Renderer\HtmlRenderer;
|
||||||
|
use App\Export\Renderer\HtmlRendererFactory;
|
||||||
|
use App\Export\Renderer\PdfRendererFactory;
|
||||||
use App\Export\ServiceExport;
|
use App\Export\ServiceExport;
|
||||||
use App\Kernel;
|
use App\Kernel;
|
||||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Component\DependencyInjection\Definition;
|
||||||
use Symfony\Component\DependencyInjection\Reference;
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,5 +41,40 @@ class ExportServiceCompilerPass implements CompilerPassInterface
|
|||||||
foreach ($taggedExporter as $id => $tags) {
|
foreach ($taggedExporter as $id => $tags) {
|
||||||
$definition->addMethodCall('addTimesheetExporter', [new Reference($id)]);
|
$definition->addMethodCall('addTimesheetExporter', [new Reference($id)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$path = dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR;
|
||||||
|
foreach ($container->getParameter('kimai.export.documents') as $exportPath) {
|
||||||
|
if (!is_dir($path . $exportPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (glob($path . $exportPath . '/*.html.twig') as $htmlTpl) {
|
||||||
|
$tplName = basename($htmlTpl);
|
||||||
|
|
||||||
|
$serviceId = 'exporter_renderer.' . str_replace('.', '_', $tplName);
|
||||||
|
|
||||||
|
$factoryDefinition = new Definition(HtmlRenderer::class);
|
||||||
|
$factoryDefinition->addArgument($tplName);
|
||||||
|
$factoryDefinition->addArgument($tplName);
|
||||||
|
$factoryDefinition->setFactory([new Reference(HtmlRendererFactory::class), 'create']);
|
||||||
|
|
||||||
|
$container->setDefinition($serviceId, $factoryDefinition);
|
||||||
|
$definition->addMethodCall('addRenderer', [new Reference($serviceId)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (glob($path . $exportPath . '/*.pdf.twig') as $pdfHtml) {
|
||||||
|
$tplName = basename($pdfHtml);
|
||||||
|
|
||||||
|
$serviceId = 'exporter_renderer.' . str_replace('.', '_', $tplName);
|
||||||
|
|
||||||
|
$factoryDefinition = new Definition(HtmlRenderer::class);
|
||||||
|
$factoryDefinition->addArgument($tplName);
|
||||||
|
$factoryDefinition->addArgument($tplName);
|
||||||
|
$factoryDefinition->setFactory([new Reference(PdfRendererFactory::class), 'create']);
|
||||||
|
|
||||||
|
$container->setDefinition($serviceId, $factoryDefinition);
|
||||||
|
$definition->addMethodCall('addRenderer', [new Reference($serviceId)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class TwigContextCompilerPass implements CompilerPassInterface
|
|||||||
$saml = $container->getParameter('kimai.saml');
|
$saml = $container->getParameter('kimai.saml');
|
||||||
$twig->addMethodCall('addGlobal', ['saml', $saml]);
|
$twig->addMethodCall('addGlobal', ['saml', $saml]);
|
||||||
|
|
||||||
if ($container->hasDefinition('twig.loader.native_filesystem')) {
|
|
||||||
$definition = $container->getDefinition('twig.loader.native_filesystem');
|
$definition = $container->getDefinition('twig.loader.native_filesystem');
|
||||||
|
|
||||||
$path = dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR;
|
$path = dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR;
|
||||||
@@ -42,6 +41,12 @@ class TwigContextCompilerPass implements CompilerPassInterface
|
|||||||
}
|
}
|
||||||
$definition->addMethodCall('addPath', [$path . $invoicePath, 'invoice']);
|
$definition->addMethodCall('addPath', [$path . $invoicePath, 'invoice']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($container->getParameter('kimai.export.documents') as $exportPath) {
|
||||||
|
if (!is_dir($path . $exportPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$definition->addMethodCall('addPath', [$path . $exportPath, 'export']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->append($this->getUserNode())
|
->append($this->getUserNode())
|
||||||
->append($this->getTimesheetNode())
|
->append($this->getTimesheetNode())
|
||||||
->append($this->getInvoiceNode())
|
->append($this->getInvoiceNode())
|
||||||
|
->append($this->getExportNode())
|
||||||
->append($this->getLanguagesNode())
|
->append($this->getLanguagesNode())
|
||||||
->append($this->getCalendarNode())
|
->append($this->getCalendarNode())
|
||||||
->append($this->getThemeNode())
|
->append($this->getThemeNode())
|
||||||
@@ -236,6 +237,33 @@ class Configuration implements ConfigurationInterface
|
|||||||
return $node;
|
return $node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getExportNode()
|
||||||
|
{
|
||||||
|
$builder = new TreeBuilder('export');
|
||||||
|
/** @var ArrayNodeDefinition $node */
|
||||||
|
$node = $builder->getRootNode();
|
||||||
|
|
||||||
|
$node
|
||||||
|
->addDefaultsIfNotSet()
|
||||||
|
->children()
|
||||||
|
->arrayNode('defaults')
|
||||||
|
->scalarPrototype()->end()
|
||||||
|
->defaultValue([
|
||||||
|
'var/export/',
|
||||||
|
'templates/export/renderer/'
|
||||||
|
])
|
||||||
|
->end()
|
||||||
|
->arrayNode('documents')
|
||||||
|
->requiresAtLeastOneElement()
|
||||||
|
->scalarPrototype()->end()
|
||||||
|
->defaultValue([])
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
;
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
protected function getLanguagesNode()
|
protected function getLanguagesNode()
|
||||||
{
|
{
|
||||||
$builder = new TreeBuilder('languages');
|
$builder = new TreeBuilder('languages');
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use App\Event\ProjectMetaDisplayEvent;
|
|||||||
use App\Event\TimesheetMetaDisplayEvent;
|
use App\Event\TimesheetMetaDisplayEvent;
|
||||||
use App\Event\UserPreferenceDisplayEvent;
|
use App\Event\UserPreferenceDisplayEvent;
|
||||||
use App\Export\ExportItemInterface;
|
use App\Export\ExportItemInterface;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\CustomerQuery;
|
use App\Repository\Query\CustomerQuery;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
@@ -35,11 +36,24 @@ class HtmlRenderer
|
|||||||
* @var EventDispatcherInterface
|
* @var EventDispatcherInterface
|
||||||
*/
|
*/
|
||||||
protected $dispatcher;
|
protected $dispatcher;
|
||||||
|
/**
|
||||||
|
* @var ProjectRepository
|
||||||
|
*/
|
||||||
|
private $projectRepository;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $id = 'html';
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $template = 'default.html.twig';
|
||||||
|
|
||||||
public function __construct(Environment $twig, EventDispatcherInterface $dispatcher)
|
public function __construct(Environment $twig, EventDispatcherInterface $dispatcher, ProjectRepository $projectRepository)
|
||||||
{
|
{
|
||||||
$this->twig = $twig;
|
$this->twig = $twig;
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
|
$this->projectRepository = $projectRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,10 +101,13 @@ class HtmlRenderer
|
|||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
$userPreferences = $event->getPreferences();
|
$userPreferences = $event->getPreferences();
|
||||||
|
|
||||||
$content = $this->twig->render('export/renderer/default.html.twig', array_merge([
|
$summary = $this->calculateSummary($timesheets);
|
||||||
|
|
||||||
|
$content = $this->twig->render($this->getTemplate(), array_merge([
|
||||||
'entries' => $timesheets,
|
'entries' => $timesheets,
|
||||||
'query' => $query,
|
'query' => $query,
|
||||||
'summaries' => $this->calculateSummary($timesheets),
|
'summaries' => $summary,
|
||||||
|
'budgets' => $this->calculateProjectBudget($timesheets, $query, $this->projectRepository),
|
||||||
'timesheetMetaFields' => $timesheetMetaFields,
|
'timesheetMetaFields' => $timesheetMetaFields,
|
||||||
'customerMetaFields' => $customerMetaFields,
|
'customerMetaFields' => $customerMetaFields,
|
||||||
'projectMetaFields' => $projectMetaFields,
|
'projectMetaFields' => $projectMetaFields,
|
||||||
@@ -104,8 +121,27 @@ class HtmlRenderer
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getTemplate(): string
|
||||||
|
{
|
||||||
|
return '@export/' . $this->template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTemplate(string $filename): HtmlRenderer
|
||||||
|
{
|
||||||
|
$this->template = $filename;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setId(string $id): HtmlRenderer
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): string
|
public function getId(): string
|
||||||
{
|
{
|
||||||
return 'html';
|
return $this->id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Export\Base;
|
namespace App\Export\Base;
|
||||||
|
|
||||||
use App\Export\ExportItemInterface;
|
use App\Export\ExportItemInterface;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Timesheet\UserDateTimeFactory;
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
use App\Utils\HtmlToPdfConverter;
|
use App\Utils\HtmlToPdfConverter;
|
||||||
@@ -33,17 +34,30 @@ class PDFRenderer
|
|||||||
* @var HtmlToPdfConverter
|
* @var HtmlToPdfConverter
|
||||||
*/
|
*/
|
||||||
private $converter;
|
private $converter;
|
||||||
|
/**
|
||||||
|
* @var ProjectRepository
|
||||||
|
*/
|
||||||
|
private $projectRepository;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $id = 'pdf';
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $template = 'default.pdf.twig';
|
||||||
|
|
||||||
public function __construct(Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter)
|
public function __construct(Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter, ProjectRepository $projectRepository)
|
||||||
{
|
{
|
||||||
$this->twig = $twig;
|
$this->twig = $twig;
|
||||||
$this->dateTime = $dateTime;
|
$this->dateTime = $dateTime;
|
||||||
$this->converter = $converter;
|
$this->converter = $converter;
|
||||||
|
$this->projectRepository = $projectRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTemplate(): string
|
protected function getTemplate(): string
|
||||||
{
|
{
|
||||||
return 'export/renderer/pdf.html.twig';
|
return '@export/' . $this->template;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getOptions(TimesheetQuery $query): array
|
protected function getOptions(TimesheetQuery $query): array
|
||||||
@@ -68,11 +82,13 @@ class PDFRenderer
|
|||||||
*/
|
*/
|
||||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||||
{
|
{
|
||||||
|
$summary = $this->calculateSummary($timesheets);
|
||||||
$content = $this->twig->render($this->getTemplate(), array_merge([
|
$content = $this->twig->render($this->getTemplate(), array_merge([
|
||||||
'entries' => $timesheets,
|
'entries' => $timesheets,
|
||||||
'query' => $query,
|
'query' => $query,
|
||||||
'now' => $this->dateTime->createDateTime(),
|
'now' => $this->dateTime->createDateTime(),
|
||||||
'summaries' => $this->calculateSummary($timesheets),
|
'summaries' => $summary,
|
||||||
|
'budgets' => $this->calculateProjectBudget($timesheets, $query, $this->projectRepository),
|
||||||
'decimal' => false,
|
'decimal' => false,
|
||||||
], $this->getOptions($query)));
|
], $this->getOptions($query)));
|
||||||
|
|
||||||
@@ -88,8 +104,22 @@ class PDFRenderer
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setTemplate(string $filename): PDFRenderer
|
||||||
|
{
|
||||||
|
$this->template = $filename;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setId(string $id): PDFRenderer
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): string
|
public function getId(): string
|
||||||
{
|
{
|
||||||
return 'pdf';
|
return $this->id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
namespace App\Export\Base;
|
namespace App\Export\Base;
|
||||||
|
|
||||||
use App\Export\ExportItemInterface;
|
use App\Export\ExportItemInterface;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
|
||||||
trait RendererTrait
|
trait RendererTrait
|
||||||
{
|
{
|
||||||
@@ -80,4 +82,52 @@ trait RendererTrait
|
|||||||
|
|
||||||
return $summary;
|
return $summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ExportItemInterface[] $exportItems
|
||||||
|
* @param TimesheetQuery $query
|
||||||
|
* @param ProjectRepository $projectRepository
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function calculateProjectBudget(array $exportItems, TimesheetQuery $query, ProjectRepository $projectRepository)
|
||||||
|
{
|
||||||
|
$summary = [];
|
||||||
|
|
||||||
|
foreach ($exportItems as $exportItem) {
|
||||||
|
$customer = null;
|
||||||
|
$customerId = 'none';
|
||||||
|
$project = null;
|
||||||
|
$projectId = 'none';
|
||||||
|
|
||||||
|
if (null !== ($project = $exportItem->getProject())) {
|
||||||
|
$customer = $project->getCustomer();
|
||||||
|
$customerId = $customer->getId();
|
||||||
|
$projectId = $project->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $customerId . '_' . $projectId;
|
||||||
|
|
||||||
|
if (!isset($summary[$id])) {
|
||||||
|
$summary[$id] = [
|
||||||
|
'time' => $project->getTimeBudget(),
|
||||||
|
'money' => $project->getBudget(),
|
||||||
|
'time_left' => null,
|
||||||
|
'money_left' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (null !== $project && ($project->getTimeBudget() > 0 || $project->getBudget() > 0)) {
|
||||||
|
$projectStats = $projectRepository->getProjectStatistics($project, null, $query->getEnd());
|
||||||
|
|
||||||
|
if ($project->getTimeBudget() > 0) {
|
||||||
|
$summary[$id]['time_left'] = $project->getTimeBudget() - $projectStats->getRecordDuration();
|
||||||
|
}
|
||||||
|
if ($project->getBudget() > 0) {
|
||||||
|
$summary[$id]['money_left'] = $project->getBudget() - $projectStats->getRecordRate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/Export/ExportRendererInterface.php
Normal file
38
src/Export/ExportRendererInterface.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?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\Export;
|
||||||
|
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
interface ExportRendererInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ExportItemInterface[] $exportItems
|
||||||
|
* @param TimesheetQuery $query
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function render(array $exportItems, TimesheetQuery $query): Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getId(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIcon(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTitle(): string;
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
namespace App\Export\Renderer;
|
namespace App\Export\Renderer;
|
||||||
|
|
||||||
use App\Export\Base\HtmlRenderer as BaseHtmlRenderer;
|
use App\Export\Base\HtmlRenderer as BaseHtmlRenderer;
|
||||||
use App\Export\RendererInterface;
|
use App\Export\ExportRendererInterface;
|
||||||
|
|
||||||
final class HtmlRenderer extends BaseHtmlRenderer implements RendererInterface
|
final class HtmlRenderer extends BaseHtmlRenderer implements ExportRendererInterface
|
||||||
{
|
{
|
||||||
public function getIcon(): string
|
public function getIcon(): string
|
||||||
{
|
{
|
||||||
|
|||||||
46
src/Export/Renderer/HtmlRendererFactory.php
Normal file
46
src/Export/Renderer/HtmlRendererFactory.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
final class HtmlRendererFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Environment
|
||||||
|
*/
|
||||||
|
private $twig;
|
||||||
|
/**
|
||||||
|
* @var EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
private $dispatcher;
|
||||||
|
/**
|
||||||
|
* @var ProjectRepository
|
||||||
|
*/
|
||||||
|
private $projectRepository;
|
||||||
|
|
||||||
|
public function __construct(Environment $twig, EventDispatcherInterface $dispatcher, ProjectRepository $projectRepository)
|
||||||
|
{
|
||||||
|
$this->twig = $twig;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
|
$this->projectRepository = $projectRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(string $id, string $template): HtmlRenderer
|
||||||
|
{
|
||||||
|
$renderer = new HtmlRenderer($this->twig, $this->dispatcher, $this->projectRepository);
|
||||||
|
$renderer->setId($id);
|
||||||
|
$renderer->setTemplate($template);
|
||||||
|
|
||||||
|
return $renderer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
namespace App\Export\Renderer;
|
namespace App\Export\Renderer;
|
||||||
|
|
||||||
use App\Export\Base\PDFRenderer as BasePDFRenderer;
|
use App\Export\Base\PDFRenderer as BasePDFRenderer;
|
||||||
use App\Export\RendererInterface;
|
use App\Export\ExportRendererInterface;
|
||||||
|
|
||||||
final class PDFRenderer extends BasePDFRenderer implements RendererInterface
|
final class PDFRenderer extends BasePDFRenderer implements ExportRendererInterface
|
||||||
{
|
{
|
||||||
public function getIcon(): string
|
public function getIcon(): string
|
||||||
{
|
{
|
||||||
|
|||||||
52
src/Export/Renderer/PdfRendererFactory.php
Normal file
52
src/Export/Renderer/PdfRendererFactory.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
|
use App\Utils\HtmlToPdfConverter;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
final class PdfRendererFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Environment
|
||||||
|
*/
|
||||||
|
private $twig;
|
||||||
|
/**
|
||||||
|
* @var UserDateTimeFactory
|
||||||
|
*/
|
||||||
|
private $dateTime;
|
||||||
|
/**
|
||||||
|
* @var HtmlToPdfConverter
|
||||||
|
*/
|
||||||
|
private $converter;
|
||||||
|
/**
|
||||||
|
* @var ProjectRepository
|
||||||
|
*/
|
||||||
|
private $projectRepository;
|
||||||
|
|
||||||
|
public function __construct(Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter, ProjectRepository $projectRepository)
|
||||||
|
{
|
||||||
|
$this->twig = $twig;
|
||||||
|
$this->dateTime = $dateTime;
|
||||||
|
$this->converter = $converter;
|
||||||
|
$this->projectRepository = $projectRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(string $id, string $template): PDFRenderer
|
||||||
|
{
|
||||||
|
$renderer = new PDFRenderer($this->twig, $this->dateTime, $this->converter, $this->projectRepository);
|
||||||
|
$renderer->setId($id);
|
||||||
|
$renderer->setTemplate($template);
|
||||||
|
|
||||||
|
return $renderer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,30 +9,6 @@
|
|||||||
|
|
||||||
namespace App\Export;
|
namespace App\Export;
|
||||||
|
|
||||||
use App\Repository\Query\TimesheetQuery;
|
interface RendererInterface extends ExportRendererInterface
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
|
|
||||||
interface RendererInterface
|
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @param ExportItemInterface[] $exportItems
|
|
||||||
* @param TimesheetQuery $query
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function render(array $exportItems, TimesheetQuery $query): Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getId(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getIcon(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getTitle(): string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace App\Export;
|
|||||||
final class ServiceExport
|
final class ServiceExport
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var RendererInterface[]
|
* @var ExportRendererInterface[]
|
||||||
*/
|
*/
|
||||||
private $renderer = [];
|
private $renderer = [];
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ final class ServiceExport
|
|||||||
*/
|
*/
|
||||||
private $exporter = [];
|
private $exporter = [];
|
||||||
|
|
||||||
public function addRenderer(RendererInterface $renderer): ServiceExport
|
public function addRenderer(ExportRendererInterface $renderer): ServiceExport
|
||||||
{
|
{
|
||||||
$this->renderer[] = $renderer;
|
$this->renderer[] = $renderer;
|
||||||
|
|
||||||
@@ -29,14 +29,14 @@ final class ServiceExport
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return RendererInterface[]
|
* @return ExportRendererInterface[]
|
||||||
*/
|
*/
|
||||||
public function getRenderer(): array
|
public function getRenderer(): array
|
||||||
{
|
{
|
||||||
return $this->renderer;
|
return $this->renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRendererById(string $id): ?RendererInterface
|
public function getRendererById(string $id): ?ExportRendererInterface
|
||||||
{
|
{
|
||||||
foreach ($this->renderer as $renderer) {
|
foreach ($this->renderer as $renderer) {
|
||||||
if ($renderer->getId() === $id) {
|
if ($renderer->getId() === $id) {
|
||||||
|
|||||||
@@ -73,20 +73,31 @@ class ProjectRepository extends EntityRepository
|
|||||||
return $this->count([]);
|
return $this->count([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProjectStatistics(Project $project): ProjectStatistic
|
public function getProjectStatistics(Project $project, ?\DateTime $begin = null, ?\DateTime $end = null): ProjectStatistic
|
||||||
{
|
{
|
||||||
$stats = new ProjectStatistic($project);
|
$stats = new ProjectStatistic($project);
|
||||||
|
|
||||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
|
||||||
$qb
|
$qb
|
||||||
|
->from(Timesheet::class, 't')
|
||||||
->addSelect('COUNT(t.id) as recordAmount')
|
->addSelect('COUNT(t.id) as recordAmount')
|
||||||
->addSelect('SUM(t.duration) as recordDuration')
|
->addSelect('SUM(t.duration) as recordDuration')
|
||||||
->addSelect('SUM(t.rate) as recordRate')
|
->addSelect('SUM(t.rate) as recordRate')
|
||||||
->from(Timesheet::class, 't')
|
|
||||||
->andWhere('t.project = :project')
|
->andWhere('t.project = :project')
|
||||||
|
->setParameter('project', $project)
|
||||||
;
|
;
|
||||||
$timesheetResult = $qb->getQuery()->execute(['project' => $project], Query::HYDRATE_ARRAY);
|
|
||||||
|
if (null !== $begin) {
|
||||||
|
$qb->andWhere($qb->expr()->gte('t.begin', ':begin'))
|
||||||
|
->setParameter('begin', $begin);
|
||||||
|
}
|
||||||
|
if (null !== $end) {
|
||||||
|
$qb->andWhere($qb->expr()->lte('t.end', ':end'))
|
||||||
|
->setParameter('end', $end);
|
||||||
|
}
|
||||||
|
|
||||||
|
$timesheetResult = $qb->getQuery()->getArrayResult();
|
||||||
|
|
||||||
if (isset($timesheetResult[0])) {
|
if (isset($timesheetResult[0])) {
|
||||||
$stats->setRecordAmount($timesheetResult[0]['recordAmount']);
|
$stats->setRecordAmount($timesheetResult[0]['recordAmount']);
|
||||||
|
|||||||
@@ -42,11 +42,35 @@
|
|||||||
{{ form_row(form.markAsExported) }}
|
{{ form_row(form.markAsExported) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block box_footer%}
|
{% block box_footer%}
|
||||||
<div class="btn-group" id="export-buttons" role="group">
|
{% set buttons = {} %}
|
||||||
{% for button in renderer %}
|
{% for button in renderer %}
|
||||||
|
{% set title = button.title %}
|
||||||
|
{% set group = [] %}
|
||||||
|
{% if buttons[(title)] is defined %}
|
||||||
|
{% set group = buttons[(title)] %}
|
||||||
|
{% endif %}
|
||||||
|
{% set group = group|merge([button]) %}
|
||||||
|
{% set buttons = buttons|merge({(title): group}) %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="btn-group" id="export-buttons" role="group">
|
||||||
|
{% for group in buttons %}
|
||||||
|
{% set button = group.0 %}
|
||||||
|
{% if group|length == 1 %}
|
||||||
<button type="button" id="export-{{ button.id }}-button" class="btn btn-success startExportBtn" data-type="{{ button.id }}">
|
<button type="button" id="export-{{ button.id }}-button" class="btn btn-success startExportBtn" data-type="{{ button.id }}">
|
||||||
{{ ('button.' ~ button.title)|trans }}
|
{{ ('button.' ~ button.title)|trans }}
|
||||||
</button>
|
</button>
|
||||||
|
{% elseif group|length > 1 %}
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
{{ ('button.' ~ button.title)|trans }} <span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for button in group %}
|
||||||
|
<li><a href="#" class="startExportBtn" data-type="{{ button.id }}">{{ (button.id)|trans({}, 'export') }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{{ form_widget(form.preview) }}
|
{{ form_widget(form.preview) }}
|
||||||
|
|||||||
12
templates/export/renderer/default-budget.pdf.twig
Normal file
12
templates/export/renderer/default-budget.pdf.twig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'export/renderer/default.pdf.twig' %}
|
||||||
|
{% block summary %}
|
||||||
|
{% for budget in budgets %}
|
||||||
|
{% if budget.money_left > 0 %}
|
||||||
|
{% set showRateBudget = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% if budget.time_left > 0 %}
|
||||||
|
{% set showTimeBudget = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{{ parent() }}
|
||||||
|
{% endblock %}
|
||||||
@@ -2,6 +2,17 @@
|
|||||||
{% import "macros/datatables.html.twig" as tables %}
|
{% import "macros/datatables.html.twig" as tables %}
|
||||||
{% extends 'export/layout.html.twig' %}
|
{% extends 'export/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% set showRateBudget = false %}
|
||||||
|
{% set showTimeBudget = false %}
|
||||||
|
{% for budget in budgets %}
|
||||||
|
{% if budget.money_left > 0 %}
|
||||||
|
{% set showRateBudget = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% if budget.time_left > 0 %}
|
||||||
|
{% set showTimeBudget = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% set decimal = decimal|default(false) %}
|
{% set decimal = decimal|default(false) %}
|
||||||
{% set columnTitles = {} %}
|
{% set columnTitles = {} %}
|
||||||
{% set columns = {
|
{% set columns = {
|
||||||
@@ -52,6 +63,18 @@
|
|||||||
document.getElementById('summary-show').addEventListener('click', function(event) {
|
document.getElementById('summary-show').addEventListener('click', function(event) {
|
||||||
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
|
document.getElementById('export-summary').style.display = event.target.checked ? 'block' : 'none';
|
||||||
});
|
});
|
||||||
|
document.getElementById('summary-timeBudget').addEventListener('click', function(event) {
|
||||||
|
var cells = document.getElementsByClassName('export-timeBudget');
|
||||||
|
for (var columnCell of cells) {
|
||||||
|
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('summary-budget').addEventListener('click', function(event) {
|
||||||
|
var cells = document.getElementsByClassName('export-budget');
|
||||||
|
for (var columnCell of cells) {
|
||||||
|
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
document.getElementById('summary-duration').addEventListener('click', function(event) {
|
document.getElementById('summary-duration').addEventListener('click', function(event) {
|
||||||
if (!document.getElementById('summary-rate').checked) {
|
if (!document.getElementById('summary-rate').checked) {
|
||||||
return;
|
return;
|
||||||
@@ -131,6 +154,23 @@
|
|||||||
{{ 'label.activity'|trans }}
|
{{ 'label.activity'|trans }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="summary-timeBudget">
|
||||||
|
<input type="checkbox" id="summary-timeBudget" name="summary-timeBudget" checked>
|
||||||
|
{{ 'label.timeBudget'|trans }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label" for="summary-budget">
|
||||||
|
<input type="checkbox" id="summary-budget" name="summary-budget" checked>
|
||||||
|
{{ 'label.budget'|trans }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label" for="summary-duration">
|
<label class="control-label" for="summary-duration">
|
||||||
<input type="checkbox" id="summary-duration" name="summary-duration" checked>
|
<input type="checkbox" id="summary-duration" name="summary-duration" checked>
|
||||||
@@ -185,6 +225,12 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{{ 'label.customer'|trans }}</th>
|
<th>{{ 'label.customer'|trans }}</th>
|
||||||
<th>{{ 'label.project'|trans }}</th>
|
<th>{{ 'label.project'|trans }}</th>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<th class="center export-timeBudget">{{ 'label.timeBudget'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<th class="center export-budget">{{ 'label.budget'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
<th class="duration summary-duration">{{ 'label.duration'|trans }}</th>
|
<th class="duration summary-duration">{{ 'label.duration'|trans }}</th>
|
||||||
<th class="cost summary-rate">{{ 'label.rate'|trans }}</th>
|
<th class="cost summary-rate">{{ 'label.rate'|trans }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -194,7 +240,7 @@
|
|||||||
{% set customerDuration = 0 %}
|
{% set customerDuration = 0 %}
|
||||||
{% set customerRate = 0 %}
|
{% set customerRate = 0 %}
|
||||||
{% set customerCurrency = null %}
|
{% set customerCurrency = null %}
|
||||||
{% for summary in summaries %}
|
{% for id, summary in summaries %}
|
||||||
{% if customer is same as(null) %}
|
{% if customer is same as(null) %}
|
||||||
{% set customer = summary.customer %}
|
{% set customer = summary.customer %}
|
||||||
{% set customerCurrency = summary.currency %}
|
{% set customerCurrency = summary.currency %}
|
||||||
@@ -202,6 +248,12 @@
|
|||||||
{% if customer is not same as(summary.customer) %}
|
{% if customer is not same as(summary.customer) %}
|
||||||
<tr class="summary">
|
<tr class="summary">
|
||||||
<td colspan="2"></td>
|
<td colspan="2"></td>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<td class="export-timeBudget"></td>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<td class="export-budget"></td>
|
||||||
|
{% endif %}
|
||||||
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
|
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
|
||||||
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
|
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -213,6 +265,20 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ summary.customer }}</td>
|
<td>{{ summary.customer }}</td>
|
||||||
<td>{{ summary.project }}</td>
|
<td>{{ summary.project }}</td>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<td class="center export-timeBudget">
|
||||||
|
{% if budgets[id] is defined and budgets[id].time_left > 0 %}
|
||||||
|
{{ budgets[id].time_left|duration(decimal) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<td class="center export-budget">
|
||||||
|
{% if budgets[id] is defined and budgets[id].money_left > 0 %}
|
||||||
|
{{ budgets[id].money_left|money(summary.currency) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
<td class="duration summary-duration">{{ summary.duration|duration(decimal) }}</td>
|
<td class="duration summary-duration">{{ summary.duration|duration(decimal) }}</td>
|
||||||
<td class="cost summary-rate">{{ summary.rate|money(summary.currency) }}</td>
|
<td class="cost summary-rate">{{ summary.rate|money(summary.currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -222,6 +288,12 @@
|
|||||||
{% if customer is not same as(null) %}
|
{% if customer is not same as(null) %}
|
||||||
<tr class="summary">
|
<tr class="summary">
|
||||||
<td colspan="2"></td>
|
<td colspan="2"></td>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<td class="export-timeBudget"></td>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<td class="export-budget"></td>
|
||||||
|
{% endif %}
|
||||||
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
|
<td class="totals duration summary-duration">{{ customerDuration|duration(decimal) }}</td>
|
||||||
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
|
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
274
templates/export/renderer/default.pdf.twig
Normal file
274
templates/export/renderer/default.pdf.twig
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
{% set showUserColumn = true %}
|
||||||
|
{% set showRateColumn = true %}
|
||||||
|
{% set decimal = decimal|default(false) %}
|
||||||
|
{% set showRateBudget = false %}
|
||||||
|
{% set showTimeBudget = false %}
|
||||||
|
{% if query.user %}
|
||||||
|
{# this is only triggered, if a user exports from his personal timesheet screen#}
|
||||||
|
{% set showUserColumn = false %}
|
||||||
|
{% set showRateColumn = is_granted('view_rate_own_timesheet') %}
|
||||||
|
{# TODO if exporting via the admin screen, users without view_rate_own_timesheet might still see their own rates, maybe merge view_rate_own_timesheet and view_rate_other_timesheet into a new view_rate permission? #}
|
||||||
|
{% endif %}
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
table.items {
|
||||||
|
border: 0.1mm solid #000000;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 9pt;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.items td {
|
||||||
|
border-left: 0.1mm solid #000000;
|
||||||
|
border-right: 0.1mm solid #000000;
|
||||||
|
}
|
||||||
|
.items tr.even {
|
||||||
|
/*background-color: #e0ebff;*/
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.items tr.summary {
|
||||||
|
background-color: #efefef;
|
||||||
|
}
|
||||||
|
.items tr.summary td {
|
||||||
|
font-weight: bold;
|
||||||
|
border-top: 0.1mm solid #000000;
|
||||||
|
border-bottom: 0.1mm solid #000000;
|
||||||
|
}
|
||||||
|
table thead th {
|
||||||
|
background-color: #ececec;
|
||||||
|
border: 0.1mm solid #000000;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 10pt;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.items td.totals {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0.1mm solid #000000;
|
||||||
|
}
|
||||||
|
.items .center,
|
||||||
|
.items td.duration,
|
||||||
|
.items td.cost {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.text-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--mpdf
|
||||||
|
<htmlpagefooter name="myfooter">
|
||||||
|
<table style="border-top: 1px solid #000000; font-size: 9pt; padding-top: 3mm; width: 100%">
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
|
||||||
|
{% if not showUserColumn %}
|
||||||
|
–
|
||||||
|
{{ 'label.user'|trans }}: {{ query.user.displayName }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td align="right">
|
||||||
|
{% if kimai_context.branding.company is not empty %}
|
||||||
|
{{ kimai_context.branding.company|raw }} – {{ now|date_full }}
|
||||||
|
{% else %}
|
||||||
|
{{ 'export.date_copyright'|trans({'%date%': now|date_full, '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</htmlpagefooter>
|
||||||
|
<sethtmlpagefooter name="myfooter" value="on" />
|
||||||
|
mpdf-->
|
||||||
|
{% block summary %}
|
||||||
|
<h2 style="margin-bottom: 0; padding-bottom: 0">{{ 'export.document_title'|trans }}</h2>
|
||||||
|
<p>
|
||||||
|
{{ 'export.period'|trans }}:
|
||||||
|
{{ query.begin|date_short }} - {{ query.end|date_short }}
|
||||||
|
</p>
|
||||||
|
<h3>{{ 'export.summary'|trans }}</h3>
|
||||||
|
<table class="items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'label.customer'|trans }}</th>
|
||||||
|
<th>{{ 'label.project'|trans }}</th>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<th class="center">{{ 'label.timeBudget'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<th class="center">{{ 'label.budget'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
|
<th class="center">{{ 'label.duration'|trans }}</th>
|
||||||
|
{% if showRateColumn %}
|
||||||
|
<th class="center">{{ 'label.rate'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% set customer = null %}
|
||||||
|
{% set customerDuration = 0 %}
|
||||||
|
{% set customerRate = 0 %}
|
||||||
|
{% set customerCurrency = null %}
|
||||||
|
{% set customerCount = 0 %}
|
||||||
|
{% for id, summary in summaries %}
|
||||||
|
{% if customer is same as(null) %}
|
||||||
|
{% set customer = summary.customer %}
|
||||||
|
{% set customerCurrency = summary.currency %}
|
||||||
|
{% endif %}
|
||||||
|
{% if customer is not same as(summary.customer) %}
|
||||||
|
<tr class="summary">
|
||||||
|
<td colspan="2">
|
||||||
|
</td>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
|
||||||
|
{% if showRateColumn %}
|
||||||
|
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% set customerCurrency = summary.currency %}
|
||||||
|
{% set customer = summary.customer %}
|
||||||
|
{% set customerDuration = 0 %}
|
||||||
|
{% set customerRate = 0 %}
|
||||||
|
{% set customerCount = 0 %}
|
||||||
|
{% endif %}
|
||||||
|
<tr class="{{ cycle(['odd', 'even'], customerCount) }}">
|
||||||
|
<td>{{ summary.customer }}</td>
|
||||||
|
<td>{{ summary.project }}</td>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<td class="center">
|
||||||
|
{% if budgets[id] is defined and budgets[id].time_left > 0 %}
|
||||||
|
{{ budgets[id].time_left|duration(decimal) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<td class="center">
|
||||||
|
{% if budgets[id] is defined and budgets[id].money_left > 0 %}
|
||||||
|
{{ budgets[id].money_left|money(summary.currency) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="duration">{{ summary.duration|duration(decimal) }}</td>
|
||||||
|
{% if showRateColumn %}
|
||||||
|
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% set customerDuration = customerDuration + summary.duration %}
|
||||||
|
{% set customerRate = customerRate + summary.rate %}
|
||||||
|
{% set customerCount = customerCount + 1 %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if customer is not same as(null) %}
|
||||||
|
<tr class="summary">
|
||||||
|
<td colspan="2"></td>
|
||||||
|
{% if showTimeBudget %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
{% if showRateBudget %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
|
||||||
|
{% if showRateColumn %}
|
||||||
|
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<pagebreak>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block items %}
|
||||||
|
<h3>{{ 'export.full_list'|trans }}</h3>
|
||||||
|
|
||||||
|
{% set duration = 0 %}
|
||||||
|
{% set rate = 0 %}
|
||||||
|
{% set currency = false %}
|
||||||
|
<table class="items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'label.date'|trans }}</th>
|
||||||
|
{% if showUserColumn %}
|
||||||
|
<th>{{ 'label.user'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
|
<th>{{ 'label.description'|trans }}</th>
|
||||||
|
<th class="center">{{ 'label.duration'|trans }}</th>
|
||||||
|
{% if showRateColumn %}
|
||||||
|
<th class="center">{{ 'label.rate'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in entries %}
|
||||||
|
{% set duration = duration + entry.duration %}
|
||||||
|
{% if currency is same as(false) %}
|
||||||
|
{% set currency = entry.project.customer.currency %}
|
||||||
|
{% endif %}
|
||||||
|
{% if currency is not same as(entry.project.customer.currency) %}
|
||||||
|
{% set currency = null %}
|
||||||
|
{% endif %}
|
||||||
|
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{{ entry.begin|date_time }}
|
||||||
|
{% if entry.end %}
|
||||||
|
<br>
|
||||||
|
{{ entry.end|date_time }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% if showUserColumn %}
|
||||||
|
<td>{{ entry.user.displayName }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
{{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %}
|
||||||
|
{% if entry.description is not empty %}
|
||||||
|
<br>
|
||||||
|
<i>{{ entry.description|escape|desc2html }}</i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="duration">{{ entry.duration|duration(decimal) }}</td>
|
||||||
|
{% if showRateColumn %}
|
||||||
|
<td class="cost">
|
||||||
|
{% if is_granted('view_rate', entry) %}
|
||||||
|
{% set rate = rate + entry.rate %}
|
||||||
|
{{ entry.rate|money(entry.project.customer.currency) }}
|
||||||
|
{% else %}
|
||||||
|
–
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr class="summary">
|
||||||
|
{% if showUserColumn %}
|
||||||
|
<td colspan="3"></td>
|
||||||
|
{% else %}
|
||||||
|
<td colspan="2"></td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="totals duration">{{ duration|duration(decimal) }}</td>
|
||||||
|
{% if showRateColumn %}
|
||||||
|
<td class="totals cost">{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
{% set showUserColumn = true %}
|
|
||||||
{% set showRateColumn = true %}
|
|
||||||
{% set decimal = decimal|default(false) %}
|
|
||||||
{% if query.user %}
|
|
||||||
{# this is only triggered, if a user exports from his personal timesheet screen#}
|
|
||||||
{% set showUserColumn = false %}
|
|
||||||
{% set showRateColumn = is_granted('view_rate_own_timesheet') %}
|
|
||||||
{# TODO if exporting via the admin screen, users without view_rate_own_timesheet might still see their own rates, maybe merge view_rate_own_timesheet and view_rate_other_timesheet into a new view_rate permission? #}
|
|
||||||
{% endif %}
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
table.items {
|
|
||||||
border: 0.1mm solid #000000;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 9pt;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
td, th {
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
.items td {
|
|
||||||
border-left: 0.1mm solid #000000;
|
|
||||||
border-right: 0.1mm solid #000000;
|
|
||||||
}
|
|
||||||
.items tr.even {
|
|
||||||
/*background-color: #e0ebff;*/
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
.items tr.summary {
|
|
||||||
background-color: #efefef;
|
|
||||||
}
|
|
||||||
.items tr.summary td {
|
|
||||||
font-weight: bold;
|
|
||||||
border-top: 0.1mm solid #000000;
|
|
||||||
border-bottom: 0.1mm solid #000000;
|
|
||||||
}
|
|
||||||
table thead th {
|
|
||||||
background-color: #ececec;
|
|
||||||
border: 0.1mm solid #000000;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 10pt;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.items td.totals {
|
|
||||||
font-weight: bold;
|
|
||||||
border: 0.1mm solid #000000;
|
|
||||||
}
|
|
||||||
.items .center,
|
|
||||||
.items td.duration,
|
|
||||||
.items td.cost {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.text-nowrap {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!--mpdf
|
|
||||||
<htmlpagefooter name="myfooter">
|
|
||||||
<table style="border-top: 1px solid #000000; font-size: 9pt; padding-top: 3mm; width: 100%">
|
|
||||||
<tr>
|
|
||||||
<td align="left">
|
|
||||||
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
|
|
||||||
{% if not showUserColumn %}
|
|
||||||
–
|
|
||||||
{{ 'label.user'|trans }}: {{ query.user.displayName }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td align="right">
|
|
||||||
{% if kimai_context.branding.company is not empty %}
|
|
||||||
{{ kimai_context.branding.company|raw }} – {{ now|date_full }}
|
|
||||||
{% else %}
|
|
||||||
{{ 'export.date_copyright'|trans({'%date%': now|date_full, '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</htmlpagefooter>
|
|
||||||
<sethtmlpagefooter name="myfooter" value="on" />
|
|
||||||
mpdf-->
|
|
||||||
<h2 style="margin-bottom: 0; padding-bottom: 0">{{ 'export.document_title'|trans }}</h2>
|
|
||||||
<p>
|
|
||||||
{{ 'export.period'|trans }}:
|
|
||||||
{{ query.begin|date_short }} - {{ query.end|date_short }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>{{ 'export.summary'|trans }}</h3>
|
|
||||||
<table class="items">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ 'label.customer'|trans }}</th>
|
|
||||||
<th>{{ 'label.project'|trans }}</th>
|
|
||||||
<th class="center">{{ 'label.duration'|trans }}</th>
|
|
||||||
{% if showRateColumn %}
|
|
||||||
<th class="center">{{ 'label.rate'|trans }}</th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% set customer = null %}
|
|
||||||
{% set customerDuration = 0 %}
|
|
||||||
{% set customerRate = 0 %}
|
|
||||||
{% set customerCurrency = null %}
|
|
||||||
{% set customerCount = 0 %}
|
|
||||||
{% for summary in summaries %}
|
|
||||||
{% if customer is same as(null) %}
|
|
||||||
{% set customer = summary.customer %}
|
|
||||||
{% set customerCurrency = summary.currency %}
|
|
||||||
{% endif %}
|
|
||||||
{% if customer is not same as(summary.customer) %}
|
|
||||||
<tr class="summary">
|
|
||||||
<td colspan="2"></td>
|
|
||||||
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
|
|
||||||
{% if showRateColumn %}
|
|
||||||
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% set customerCurrency = summary.currency %}
|
|
||||||
{% set customer = summary.customer %}
|
|
||||||
{% set customerDuration = 0 %}
|
|
||||||
{% set customerRate = 0 %}
|
|
||||||
{% set customerCount = 0 %}
|
|
||||||
{% endif %}
|
|
||||||
<tr class="{{ cycle(['odd', 'even'], customerCount) }}">
|
|
||||||
<td>{{ summary.customer }}</td>
|
|
||||||
<td>{{ summary.project }}</td>
|
|
||||||
<td class="duration">{{ summary.duration|duration(decimal) }}</td>
|
|
||||||
{% if showRateColumn %}
|
|
||||||
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% set customerDuration = customerDuration + summary.duration %}
|
|
||||||
{% set customerRate = customerRate + summary.rate %}
|
|
||||||
{% set customerCount = customerCount + 1 %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if customer is not same as(null) %}
|
|
||||||
<tr class="summary">
|
|
||||||
<td colspan="2"></td>
|
|
||||||
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
|
|
||||||
{% if showRateColumn %}
|
|
||||||
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<pagebreak>
|
|
||||||
|
|
||||||
<h3>{{ 'export.full_list'|trans }}</h3>
|
|
||||||
|
|
||||||
{% set duration = 0 %}
|
|
||||||
{% set rate = 0 %}
|
|
||||||
{% set currency = false %}
|
|
||||||
<table class="items">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ 'label.date'|trans }}</th>
|
|
||||||
{% if showUserColumn %}
|
|
||||||
<th>{{ 'label.user'|trans }}</th>
|
|
||||||
{% endif %}
|
|
||||||
<th>{{ 'label.description'|trans }}</th>
|
|
||||||
<th class="center">{{ 'label.duration'|trans }}</th>
|
|
||||||
{% if showRateColumn %}
|
|
||||||
<th class="center">{{ 'label.rate'|trans }}</th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for entry in entries %}
|
|
||||||
{% set duration = duration + entry.duration %}
|
|
||||||
{% if currency is same as(false) %}
|
|
||||||
{% set currency = entry.project.customer.currency %}
|
|
||||||
{% endif %}
|
|
||||||
{% if currency is not same as(entry.project.customer.currency) %}
|
|
||||||
{% set currency = null %}
|
|
||||||
{% endif %}
|
|
||||||
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
|
||||||
<td class="text-nowrap">
|
|
||||||
{{ entry.begin|date_time }}
|
|
||||||
{% if entry.end %}
|
|
||||||
<br>
|
|
||||||
{{ entry.end|date_time }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% if showUserColumn %}
|
|
||||||
<td>{{ entry.user.displayName }}</td>
|
|
||||||
{% endif %}
|
|
||||||
<td>
|
|
||||||
{{ entry.project.customer.name }} - {{ entry.project.name }}{% if entry.activity is not null %} - {{ entry.activity.name }}{% endif %}
|
|
||||||
{% if entry.description is not empty %}
|
|
||||||
<br>
|
|
||||||
<i>{{ entry.description|escape|desc2html }}</i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="duration">{{ entry.duration|duration(decimal) }}</td>
|
|
||||||
{% if showRateColumn %}
|
|
||||||
<td class="cost">
|
|
||||||
{% if is_granted('view_rate', entry) %}
|
|
||||||
{% set rate = rate + entry.rate %}
|
|
||||||
{{ entry.rate|money(entry.project.customer.currency) }}
|
|
||||||
{% else %}
|
|
||||||
–
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
<tr class="summary">
|
|
||||||
{% if showUserColumn %}
|
|
||||||
<td colspan="3"></td>
|
|
||||||
{% else %}
|
|
||||||
<td colspan="2"></td>
|
|
||||||
{% endif %}
|
|
||||||
<td class="totals duration">{{ duration|duration(decimal) }}</td>
|
|
||||||
{% if showRateColumn %}
|
|
||||||
<td class="totals cost">{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -88,8 +88,8 @@ class ExportControllerTest extends ControllerBaseTest
|
|||||||
$this->assertDataTableRowCount($client, 'datatable_export', 23);
|
$this->assertDataTableRowCount($client, 'datatable_export', 23);
|
||||||
|
|
||||||
// assert export type buttons are available
|
// assert export type buttons are available
|
||||||
$expected = ['csv', 'html', 'pdf', 'xlsx'];
|
$expected = ['csv', 'default.html.twig', 'default-budget.pdf.twig', 'default.pdf.twig', 'xlsx'];
|
||||||
$node = $client->getCrawler()->filter('#export-buttons button');
|
$node = $client->getCrawler()->filter('#export-buttons .startExportBtn');
|
||||||
$this->assertEquals(count($expected), $node->count());
|
$this->assertEquals(count($expected), $node->count());
|
||||||
/** @var \DOMElement $button */
|
/** @var \DOMElement $button */
|
||||||
foreach ($node->getIterator() as $button) {
|
foreach ($node->getIterator() as $button) {
|
||||||
@@ -140,8 +140,8 @@ class ExportControllerTest extends ControllerBaseTest
|
|||||||
$this->assertDataTableRowCount($client, 'datatable_export', 3);
|
$this->assertDataTableRowCount($client, 'datatable_export', 3);
|
||||||
|
|
||||||
// assert export type buttons are available
|
// assert export type buttons are available
|
||||||
$expected = ['csv', 'html', 'pdf', 'xlsx'];
|
$expected = ['csv', 'default.html.twig', 'default-budget.pdf.twig', 'default.pdf.twig', 'xlsx'];
|
||||||
$node = $client->getCrawler()->filter('#export-buttons button');
|
$node = $client->getCrawler()->filter('#export-buttons .startExportBtn');
|
||||||
$this->assertEquals(count($expected), $node->count());
|
$this->assertEquals(count($expected), $node->count());
|
||||||
/** @var \DOMElement $button */
|
/** @var \DOMElement $button */
|
||||||
foreach ($node->getIterator() as $button) {
|
foreach ($node->getIterator() as $button) {
|
||||||
@@ -208,7 +208,7 @@ class ExportControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
// don't add daterange to make sure the current month is the default range
|
// don't add daterange to make sure the current month is the default range
|
||||||
$client->submit($form, [
|
$client->submit($form, [
|
||||||
'type' => 'html',
|
'type' => 'default.html.twig',
|
||||||
'markAsExported' => 1
|
'markAsExported' => 1
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -292,6 +292,14 @@ class ConfigurationTest extends TestCase
|
|||||||
'simple_form' => true,
|
'simple_form' => true,
|
||||||
'number_format' => '{Y}/{cy,3}',
|
'number_format' => '{Y}/{cy,3}',
|
||||||
],
|
],
|
||||||
|
'export' => [
|
||||||
|
'documents' => [
|
||||||
|
],
|
||||||
|
'defaults' => [
|
||||||
|
0 => 'var/export/',
|
||||||
|
1 => 'templates/export/renderer/',
|
||||||
|
],
|
||||||
|
],
|
||||||
'languages' => [],
|
'languages' => [],
|
||||||
'calendar' => [
|
'calendar' => [
|
||||||
'week_numbers' => true,
|
'week_numbers' => true,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use App\Event\ActivityMetaDisplayEvent;
|
|||||||
use App\Event\CustomerMetaDisplayEvent;
|
use App\Event\CustomerMetaDisplayEvent;
|
||||||
use App\Event\ProjectMetaDisplayEvent;
|
use App\Event\ProjectMetaDisplayEvent;
|
||||||
use App\Event\TimesheetMetaDisplayEvent;
|
use App\Event\TimesheetMetaDisplayEvent;
|
||||||
|
use App\Export\ExportRendererInterface;
|
||||||
use App\Export\RendererInterface;
|
use App\Export\RendererInterface;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Twig\DateExtensions;
|
use App\Twig\DateExtensions;
|
||||||
@@ -74,10 +75,10 @@ abstract class AbstractRendererTest extends KernelTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param RendererInterface $renderer
|
* @param ExportRendererInterface $renderer
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
*/
|
*/
|
||||||
protected function render(RendererInterface $renderer)
|
protected function render(ExportRendererInterface $renderer)
|
||||||
{
|
{
|
||||||
$customer = new Customer();
|
$customer = new Customer();
|
||||||
$customer->setName('Customer Name');
|
$customer->setName('Customer Name');
|
||||||
|
|||||||
38
tests/Export/Renderer/HtmlRendererFactoryTest.php
Normal file
38
tests/Export/Renderer/HtmlRendererFactoryTest.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?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\Tests\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\Renderer\HtmlRenderer;
|
||||||
|
use App\Export\Renderer\HtmlRendererFactory;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Export\Renderer\HtmlRendererFactory
|
||||||
|
*/
|
||||||
|
class HtmlRendererFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testCreate()
|
||||||
|
{
|
||||||
|
$sut = new HtmlRendererFactory(
|
||||||
|
$this->createMock(Environment::class),
|
||||||
|
$this->createMock(EventDispatcherInterface::class),
|
||||||
|
$this->createMock(ProjectRepository::class)
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @var HtmlRenderer $renderer */
|
||||||
|
$renderer = $sut->create('foo', 'bar.html.twig');
|
||||||
|
|
||||||
|
self::assertInstanceOf(HtmlRenderer::class, $renderer);
|
||||||
|
self::assertEquals('foo', $renderer->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Tests\Export\Renderer;
|
namespace App\Tests\Export\Renderer;
|
||||||
|
|
||||||
use App\Export\Renderer\HtmlRenderer;
|
use App\Export\Renderer\HtmlRenderer;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@@ -26,7 +27,8 @@ class HtmlRendererTest extends AbstractRendererTest
|
|||||||
{
|
{
|
||||||
$sut = new HtmlRenderer(
|
$sut = new HtmlRenderer(
|
||||||
$this->createMock(Environment::class),
|
$this->createMock(Environment::class),
|
||||||
new EventDispatcher()
|
new EventDispatcher(),
|
||||||
|
$this->createMock(ProjectRepository::class)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertEquals('html', $sut->getId());
|
$this->assertEquals('html', $sut->getId());
|
||||||
@@ -44,7 +46,7 @@ class HtmlRendererTest extends AbstractRendererTest
|
|||||||
$request->setLocale('en');
|
$request->setLocale('en');
|
||||||
$stack->push($request);
|
$stack->push($request);
|
||||||
|
|
||||||
$sut = new HtmlRenderer($twig, new EventDispatcher());
|
$sut = new HtmlRenderer($twig, new EventDispatcher(), $this->createMock(ProjectRepository::class));
|
||||||
|
|
||||||
$response = $this->render($sut);
|
$response = $this->render($sut);
|
||||||
|
|
||||||
|
|||||||
41
tests/Export/Renderer/PdfRendererFactoryTest.php
Normal file
41
tests/Export/Renderer/PdfRendererFactoryTest.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?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\Tests\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\Renderer\HtmlRenderer;
|
||||||
|
use App\Export\Renderer\PDFRenderer;
|
||||||
|
use App\Export\Renderer\PdfRendererFactory;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
|
use App\Timesheet\UserDateTimeFactory;
|
||||||
|
use App\Utils\HtmlToPdfConverter;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Export\Renderer\PdfRendererFactory
|
||||||
|
*/
|
||||||
|
class PdfRendererFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testCreate()
|
||||||
|
{
|
||||||
|
$sut = new PdfRendererFactory(
|
||||||
|
$this->createMock(Environment::class),
|
||||||
|
$this->createMock(UserDateTimeFactory::class),
|
||||||
|
$this->createMock(HtmlToPdfConverter::class),
|
||||||
|
$this->createMock(ProjectRepository::class)
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @var HtmlRenderer $renderer */
|
||||||
|
$renderer = $sut->create('foo', 'bar.pdf.twig');
|
||||||
|
|
||||||
|
self::assertInstanceOf(PDFRenderer::class, $renderer);
|
||||||
|
self::assertEquals('foo', $renderer->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Tests\Export\Renderer;
|
namespace App\Tests\Export\Renderer;
|
||||||
|
|
||||||
use App\Export\Renderer\PDFRenderer;
|
use App\Export\Renderer\PDFRenderer;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
||||||
use App\Utils\HtmlToPdfConverter;
|
use App\Utils\HtmlToPdfConverter;
|
||||||
use App\Utils\MPdfConverter;
|
use App\Utils\MPdfConverter;
|
||||||
@@ -34,7 +35,8 @@ class PdfRendererTest extends AbstractRendererTest
|
|||||||
$sut = new PDFRenderer(
|
$sut = new PDFRenderer(
|
||||||
$this->createMock(Environment::class),
|
$this->createMock(Environment::class),
|
||||||
$this->getDateTimeFactory(),
|
$this->getDateTimeFactory(),
|
||||||
$this->createMock(HtmlToPdfConverter::class)
|
$this->createMock(HtmlToPdfConverter::class),
|
||||||
|
$this->createMock(ProjectRepository::class)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertEquals('pdf', $sut->getId());
|
$this->assertEquals('pdf', $sut->getId());
|
||||||
@@ -54,7 +56,7 @@ class PdfRendererTest extends AbstractRendererTest
|
|||||||
$request->setLocale('en');
|
$request->setLocale('en');
|
||||||
$stack->push($request);
|
$stack->push($request);
|
||||||
|
|
||||||
$sut = new PDFRenderer($twig, $this->getDateTimeFactory(), $converter);
|
$sut = new PDFRenderer($twig, $this->getDateTimeFactory(), $converter, $this->createMock(ProjectRepository::class));
|
||||||
|
|
||||||
$response = $this->render($sut);
|
$response = $this->render($sut);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace App\Tests\Export;
|
|||||||
use App\Export\Renderer\HtmlRenderer;
|
use App\Export\Renderer\HtmlRenderer;
|
||||||
use App\Export\ServiceExport;
|
use App\Export\ServiceExport;
|
||||||
use App\Export\Timesheet\HtmlRenderer as HtmlExporter;
|
use App\Export\Timesheet\HtmlRenderer as HtmlExporter;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@@ -36,7 +37,7 @@ class ServiceExportTest extends TestCase
|
|||||||
{
|
{
|
||||||
$sut = new ServiceExport();
|
$sut = new ServiceExport();
|
||||||
|
|
||||||
$renderer = new HtmlRenderer($this->createMock(Environment::class), new EventDispatcher());
|
$renderer = new HtmlRenderer($this->createMock(Environment::class), new EventDispatcher(), $this->createMock(ProjectRepository::class));
|
||||||
$sut->addRenderer($renderer);
|
$sut->addRenderer($renderer);
|
||||||
|
|
||||||
self::assertEquals(1, count($sut->getRenderer()));
|
self::assertEquals(1, count($sut->getRenderer()));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Tests\Export\Timesheet;
|
namespace App\Tests\Export\Timesheet;
|
||||||
|
|
||||||
use App\Export\Timesheet\PDFRenderer;
|
use App\Export\Timesheet\PDFRenderer;
|
||||||
|
use App\Repository\ProjectRepository;
|
||||||
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
||||||
use App\Utils\HtmlToPdfConverter;
|
use App\Utils\HtmlToPdfConverter;
|
||||||
use App\Utils\MPdfConverter;
|
use App\Utils\MPdfConverter;
|
||||||
@@ -32,9 +33,10 @@ class PdfRendererTest extends AbstractRendererTest
|
|||||||
public function testConfiguration()
|
public function testConfiguration()
|
||||||
{
|
{
|
||||||
$sut = new PDFRenderer(
|
$sut = new PDFRenderer(
|
||||||
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(),
|
$this->createMock(Environment::class),
|
||||||
$this->getDateTimeFactory(),
|
$this->getDateTimeFactory(),
|
||||||
$this->getMockBuilder(HtmlToPdfConverter::class)->getMock()
|
$this->createMock(HtmlToPdfConverter::class),
|
||||||
|
$this->createMock(ProjectRepository::class)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertEquals('pdf', $sut->getId());
|
$this->assertEquals('pdf', $sut->getId());
|
||||||
@@ -52,7 +54,7 @@ class PdfRendererTest extends AbstractRendererTest
|
|||||||
$request->setLocale('en');
|
$request->setLocale('en');
|
||||||
$stack->push($request);
|
$stack->push($request);
|
||||||
|
|
||||||
$sut = new PDFRenderer($twig, $this->getDateTimeFactory(), $converter);
|
$sut = new PDFRenderer($twig, $this->getDateTimeFactory(), $converter, $this->createMock(ProjectRepository::class));
|
||||||
|
|
||||||
$response = $this->render($sut);
|
$response = $this->render($sut);
|
||||||
|
|
||||||
|
|||||||
15
translations/export.de.xlf
Normal file
15
translations/export.de.xlf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="de" datatype="plaintext" original="export.en.xlf">
|
||||||
|
<body>
|
||||||
|
<trans-unit id="default.pdf.twig">
|
||||||
|
<source>default.pdf.twig</source>
|
||||||
|
<target>Standard</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="default-budget.pdf.twig">
|
||||||
|
<source>default-budget.pdf.twig</source>
|
||||||
|
<target>Inkl. Restbudget</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
15
translations/export.en.xlf
Normal file
15
translations/export.en.xlf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" target-language="en" datatype="plaintext" original="export.en.xlf">
|
||||||
|
<body>
|
||||||
|
<trans-unit id="default.pdf.twig">
|
||||||
|
<source>default.pdf.twig</source>
|
||||||
|
<target>Standard</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="default-budget.pdf.twig">
|
||||||
|
<source>default-budget.pdf.twig</source>
|
||||||
|
<target>With remaining budget</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
Reference in New Issue
Block a user