added export module (#538)
This commit is contained in:
@@ -169,6 +169,7 @@ class TimesheetController extends BaseApiController
|
||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||
'csrf_protection' => false,
|
||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||
]);
|
||||
|
||||
$form->setData($timesheet);
|
||||
@@ -216,4 +217,64 @@ class TimesheetController extends BaseApiController
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Post(
|
||||
* description="Update an existing timesheet entry, you can pass all or just a subset of all attributes",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetFormEntity"),
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the updated timesheet entry",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity"),
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $id
|
||||
* @return Response
|
||||
*/
|
||||
public function patchAction(Request $request, string $id)
|
||||
{
|
||||
$timesheet = $this->repository->find($id);
|
||||
|
||||
if (!$this->isGranted('edit', $timesheet)) {
|
||||
throw $this->createAccessDeniedException('User cannot update timesheet');
|
||||
}
|
||||
|
||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||
'csrf_protection' => false,
|
||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||
]);
|
||||
|
||||
$form->setData($timesheet);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
if (false === $form->isValid()) {
|
||||
$view = new View($form, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
if ($form->has('duration')) {
|
||||
$duration = $form->get('duration')->getData();
|
||||
if ($duration > 0) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $form->getData();
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$record->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($timesheet);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View($timesheet, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,7 +848,7 @@ class KimaiImporterCommand extends Command
|
||||
* ["description"]=> NULL
|
||||
* ["comment"]=> string(36) "a work description"
|
||||
* -- ["commentType"]=> string(1) "0"
|
||||
* -- ["cleared"]=> string(1) "0"
|
||||
* ["cleared"]=> string(1) "0"
|
||||
* -- ["location"]=> string(0) ""
|
||||
* -- ["trackingNumber"]=> NULL
|
||||
* ["rate"]=> string(5) "50.00"
|
||||
@@ -936,6 +936,7 @@ class KimaiImporterCommand extends Command
|
||||
->setDuration($duration)
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setExported(intval($oldRecord['cleared']) !== 0)
|
||||
;
|
||||
|
||||
if (!$this->validateImport($io, $timesheet)) {
|
||||
|
||||
@@ -14,6 +14,10 @@ namespace App;
|
||||
*/
|
||||
class Constants
|
||||
{
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
public const SOFTWARE = 'Kimai 2';
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used for executing system relevant tasks.
|
||||
*
|
||||
* @Route(path="/admin/about")
|
||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||
* @Security("is_granted('system_information')")
|
||||
*/
|
||||
class AboutController extends AbstractController
|
||||
{
|
||||
|
||||
@@ -189,6 +189,7 @@ class TimesheetController extends AbstractController
|
||||
'page' => $page,
|
||||
]),
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||
'include_user' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
162
src/Controller/ExportController.php
Normal file
162
src/Controller/ExportController.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Export\ServiceExport;
|
||||
use App\Form\Toolbar\ExportToolbarForm;
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to export timesheet data.
|
||||
*
|
||||
* @Route(path="/export")
|
||||
* @Security("is_granted('view_export')")
|
||||
*/
|
||||
class ExportController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $timesheetRepository;
|
||||
|
||||
/**
|
||||
* @var ServiceExport
|
||||
*/
|
||||
protected $export;
|
||||
|
||||
/**
|
||||
* @param TimesheetRepository $timesheet
|
||||
* @param ServiceExport $export
|
||||
*/
|
||||
public function __construct(TimesheetRepository $timesheet, ServiceExport $export)
|
||||
{
|
||||
$this->timesheetRepository = $timesheet;
|
||||
$this->export = $export;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExportQuery
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDefaultQuery()
|
||||
{
|
||||
$begin = new \DateTime('first day of this month');
|
||||
$end = new \DateTime('last day of this month');
|
||||
|
||||
$query = new ExportQuery();
|
||||
$query->setOrder(ExportQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(ExportQuery::STATE_STOPPED);
|
||||
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="export", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ExportQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
$entries = $this->getEntries($query);
|
||||
|
||||
return $this->render('export/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'form' => $form->createView(),
|
||||
'renderer' => $this->export->getRenderer(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/data", name="export_data", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ExportQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
$type = $query->getType();
|
||||
if (null === $type) {
|
||||
throw $this->createNotFoundException('Missing export renderer');
|
||||
}
|
||||
|
||||
$renderer = $this->export->getRendererById($type);
|
||||
|
||||
// this code should not be reached, as the query already filters invalid values
|
||||
// when trying to call setType() with an unknown value
|
||||
if (null === $renderer) {
|
||||
throw $this->createNotFoundException('Invalid export renderer');
|
||||
}
|
||||
|
||||
$entries = $this->getEntries($query);
|
||||
|
||||
return $renderer->render($entries, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportQuery $query
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getEntries(ExportQuery $query)
|
||||
{
|
||||
$query->setResultType(ExportQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
|
||||
$queryBuilder = $this->timesheetRepository->findByQuery($query);
|
||||
|
||||
return $queryBuilder->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ExportQuery $query)
|
||||
{
|
||||
return $this->createForm(ExportToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('export', []),
|
||||
'method' => 'POST',
|
||||
'attr' => [
|
||||
'id' => 'export-form'
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -269,6 +269,7 @@ class TimesheetController extends AbstractController
|
||||
'origin' => $redirectRoute,
|
||||
]),
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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\DependencyInjection\Compiler;
|
||||
|
||||
use App\Export\ServiceExport;
|
||||
use App\Kernel;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Dynamically adds all dependencies to the ExportService.
|
||||
*/
|
||||
class ExportServiceCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
// always first check if the primary service is defined
|
||||
if (!$container->has(ServiceExport::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $container->findDefinition(ServiceExport::class);
|
||||
|
||||
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_RENDERER);
|
||||
foreach ($taggedRenderer as $id => $tags) {
|
||||
$definition->addMethodCall('addRenderer', [new Reference($id)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,7 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('date_type')->defaultValue('yyyy-MM-dd')->end() // for DateType
|
||||
->scalarNode('date_picker')->defaultValue('YYYY-MM-DD')->end() // for DateType JS component
|
||||
->scalarNode('date')->defaultValue('Y-m-d')->end() // for display via twig
|
||||
->scalarNode('date_time')->defaultValue('m-d H:i')->end() // for display via twig
|
||||
->scalarNode('duration')->defaultValue('%%h:%%m h')->end() // for display via twig
|
||||
->end()
|
||||
->end()
|
||||
|
||||
@@ -119,6 +119,14 @@ class Timesheet
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="exported", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $exported = false;
|
||||
|
||||
/**
|
||||
* Get entry id
|
||||
*
|
||||
@@ -345,6 +353,25 @@ class Timesheet
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isExported(): bool
|
||||
{
|
||||
return $this->exported;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $exported
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setExported(bool $exported)
|
||||
{
|
||||
$this->exported = $exported;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExecutionContextInterface $context
|
||||
* @param $payload
|
||||
|
||||
@@ -70,6 +70,12 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_export')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('export', 'menu.export', 'export', [], 'fas fa-file-export')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
215
src/Export/Renderer/AbstractSpreadsheetRenderer.php
Normal file
215
src/Export/Renderer/AbstractSpreadsheetRenderer.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?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\Entity\Timesheet;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\DateExtensions;
|
||||
use App\Twig\Extensions;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractSpreadsheetRenderer
|
||||
{
|
||||
/**
|
||||
* @var DateExtensions
|
||||
*/
|
||||
protected $dateExtension;
|
||||
|
||||
/**
|
||||
* @var Extensions
|
||||
*/
|
||||
protected $extension;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @param TranslatorInterface $translator
|
||||
* @param DateExtensions $dateExtension
|
||||
* @param Extensions $extensions
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->dateExtension = $dateExtension;
|
||||
$this->extension = $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDateTime(\DateTime $date)
|
||||
{
|
||||
return $this->dateExtension->dateShort($date) . ' ' . date('H:i', $date->getTimestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $amount
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedMoney($amount, $currency)
|
||||
{
|
||||
return $this->extension->money($amount, $currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @return string
|
||||
*/
|
||||
protected function getUsername(Timesheet $timesheet)
|
||||
{
|
||||
if (!empty($timesheet->getUser()->getAlias())) {
|
||||
return $timesheet->getUser()->getAlias();
|
||||
}
|
||||
|
||||
return $timesheet->getUser()->getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $seconds
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDuration($seconds)
|
||||
{
|
||||
return $this->extension->duration($seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Spreadsheet
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
*/
|
||||
protected function fromArrayToSpreadsheet(array $timesheets, TimesheetQuery $query): Spreadsheet
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$recordsHeaderColumn = 1;
|
||||
$recordsHeaderRow = 1;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.begin'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.end'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.user'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.customer'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.project'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.activity'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourly_rate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixed_rate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
|
||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||
|
||||
$durationTotal = 0;
|
||||
$currency = false;
|
||||
$rateTotal = 0;
|
||||
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$entryHeaderColumn = 1;
|
||||
|
||||
$durationTotal += $timesheet->getDuration();
|
||||
$rateTotal += $timesheet->getRate();
|
||||
if ($currency === false) {
|
||||
$currency = $timesheet->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
if ($currency !== $timesheet->getProject()->getCustomer()->getCurrency()) {
|
||||
$currency = null;
|
||||
}
|
||||
|
||||
$customerCurrency = $timesheet->getProject()->getCustomer()->getCurrency();
|
||||
|
||||
$exported = $timesheet->isExported() ? 'entryState.exported' : 'entryState.not_exported';
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDateTime($timesheet->getBegin()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDateTime($timesheet->getEnd()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getUsername($timesheet));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getProject()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getActivity()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getDescription());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->translator->trans($exported));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getHourlyRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getFixedRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDuration($timesheet->getDuration()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getRate(), $customerCurrency));
|
||||
|
||||
$entryHeaderRow++;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow(11, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
||||
$sheet->setCellValueByColumnAndRow(12, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
||||
$sheet->getCellByColumnAndRow(11, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow(11, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
|
||||
$sheet->getCellByColumnAndRow(12, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow(12, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
$spreadsheet = $this->fromArrayToSpreadsheet($timesheets, $query);
|
||||
$filename = $this->saveSpreadsheet($spreadsheet);
|
||||
|
||||
return $this->getFileResponse($filename, 'kimai-export' . $this->getFileExtension());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getFileExtension(): string;
|
||||
|
||||
/**
|
||||
* @param mixed $file
|
||||
* @param string $filename
|
||||
* @return BinaryFileResponse
|
||||
*/
|
||||
protected function getFileResponse($file, $filename): Response
|
||||
{
|
||||
$response = new BinaryFileResponse($file);
|
||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||
|
||||
$response->headers->set('Content-Type', $this->getContentType());
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
$response->deleteFileAfterSend(true);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getContentType(): string;
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return string
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||
*/
|
||||
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet): string;
|
||||
}
|
||||
71
src/Export/Renderer/CsvRenderer.php
Normal file
71
src/Export/Renderer/CsvRenderer.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?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\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return '.csv';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getContentType(): string
|
||||
{
|
||||
return 'text/csv';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return bool|string
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-csv');
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Csv');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
}
|
||||
79
src/Export/Renderer/HtmlRenderer.php
Normal file
79
src/Export/Renderer/HtmlRenderer.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Entity\Timesheet;
|
||||
use App\Export\RendererInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HtmlRenderer implements RendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
*/
|
||||
protected $twig;
|
||||
|
||||
/**
|
||||
* @param \Twig_Environment $twig
|
||||
*/
|
||||
public function __construct(\Twig_Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Runtime
|
||||
* @throws \Twig_Error_Syntax
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
$content = $this->twig->render('export/renderer/default.html.twig', [
|
||||
'entries' => $timesheets,
|
||||
'query' => $query,
|
||||
'summaries' => $this->calculateSummary($timesheets),
|
||||
]);
|
||||
|
||||
$response = new Response();
|
||||
$response->setContent($content);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return 'html';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'print';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'print';
|
||||
}
|
||||
}
|
||||
71
src/Export/Renderer/OdsRenderer.php
Normal file
71
src/Export/Renderer/OdsRenderer.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?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\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return '.ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getContentType(): string
|
||||
{
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return bool|string
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-ods');
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Ods');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
}
|
||||
94
src/Export/Renderer/PDFRenderer.php
Normal file
94
src/Export/Renderer/PDFRenderer.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?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\Entity\Timesheet;
|
||||
use App\Export\RendererInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Mpdf\Mpdf;
|
||||
use Mpdf\Output\Destination;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
|
||||
class PDFRenderer implements RendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
*/
|
||||
protected $twig;
|
||||
|
||||
/**
|
||||
* @param \Twig_Environment $twig
|
||||
*/
|
||||
public function __construct(\Twig_Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \Mpdf\MpdfException
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Runtime
|
||||
* @throws \Twig_Error_Syntax
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
$content = $this->twig->render('export/renderer/pdf.html.twig', [
|
||||
'entries' => $timesheets,
|
||||
'query' => $query,
|
||||
'now' => new \DateTime(),
|
||||
'summaries' => $this->calculateSummary($timesheets),
|
||||
]);
|
||||
|
||||
//return new Response($content);
|
||||
|
||||
$mpdf = new Mpdf();
|
||||
$mpdf->WriteHTML($content);
|
||||
$content = $mpdf->Output('test', Destination::STRING_RETURN);
|
||||
|
||||
$response = new Response($content);
|
||||
|
||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'kimai-export.pdf');
|
||||
|
||||
$response->headers->set('Content-Type', 'application/pdf');
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
}
|
||||
43
src/Export/Renderer/RendererTrait.php
Normal file
43
src/Export/Renderer/RendererTrait.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Entity\Timesheet;
|
||||
|
||||
trait RendererTrait
|
||||
{
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @return array
|
||||
*/
|
||||
protected function calculateSummary(array $timesheets)
|
||||
{
|
||||
$summary = [];
|
||||
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$id = $timesheet->getProject()->getCustomer()->getId() . '_' . $timesheet->getProject()->getId();
|
||||
if (!isset($summary[$id])) {
|
||||
$summary[$id] = [
|
||||
'customer' => $timesheet->getProject()->getCustomer()->getName(),
|
||||
'project' => $timesheet->getProject()->getName(),
|
||||
'currency' => $timesheet->getProject()->getCustomer()->getCurrency(),
|
||||
'rate' => 0,
|
||||
'duration' => 0,
|
||||
];
|
||||
}
|
||||
$summary[$id]['rate'] += $timesheet->getRate();
|
||||
$summary[$id]['duration'] += $timesheet->getDuration();
|
||||
}
|
||||
|
||||
asort($summary);
|
||||
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
71
src/Export/Renderer/XlsxRenderer.php
Normal file
71
src/Export/Renderer/XlsxRenderer.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?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\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return '.xlsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getContentType(): string
|
||||
{
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return bool|string
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-xlsx');
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
}
|
||||
39
src/Export/RendererInterface.php
Normal file
39
src/Export/RendererInterface.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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\Entity\Timesheet;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
interface RendererInterface
|
||||
{
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
}
|
||||
54
src/Export/ServiceExport.php
Normal file
54
src/Export/ServiceExport.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?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;
|
||||
|
||||
class ServiceExport
|
||||
{
|
||||
/**
|
||||
* @var RendererInterface[]
|
||||
*/
|
||||
protected $renderer = [];
|
||||
|
||||
/**
|
||||
* @param RendererInterface $renderer
|
||||
* @return $this
|
||||
*/
|
||||
public function addRenderer(RendererInterface $renderer)
|
||||
{
|
||||
$this->renderer[] = $renderer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of export renderer.
|
||||
*
|
||||
* @return RendererInterface[]
|
||||
*/
|
||||
public function getRenderer()
|
||||
{
|
||||
return $this->renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return RendererInterface|null
|
||||
*/
|
||||
public function getRendererById(string $id)
|
||||
{
|
||||
foreach ($this->renderer as $renderer) {
|
||||
if ($renderer->getId() === $id) {
|
||||
return $renderer;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use App\Form\Type\DateTimePickerType;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
@@ -226,6 +227,12 @@ class TimesheetEditForm extends AbstractType
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class);
|
||||
}
|
||||
|
||||
if ($options['include_exported']) {
|
||||
$builder->add('exported', YesNoType::class, [
|
||||
'label' => 'label.exported'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,6 +247,7 @@ class TimesheetEditForm extends AbstractType
|
||||
'csrf_token_id' => 'timesheet_edit',
|
||||
'duration_only' => $this->durationOnly,
|
||||
'include_user' => false,
|
||||
'include_exported' => false,
|
||||
'include_rate' => true,
|
||||
'docu_chapter' => 'timesheet',
|
||||
'method' => 'POST',
|
||||
|
||||
73
src/Form/Toolbar/ExportToolbarForm.php
Normal file
73
src/Form/Toolbar/ExportToolbarForm.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?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\Form\Toolbar;
|
||||
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the form used for filtering timesheet entries for exports.
|
||||
*/
|
||||
class ExportToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addExportStateChoice($builder);
|
||||
$this->addUserChoice($builder);
|
||||
$this->addDateRangeChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addExportType($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addExportType(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('type', HiddenType::class, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addExportStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('exported', ChoiceType::class, [
|
||||
'label' => 'label.exported',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.exported' => TimesheetQuery::STATE_EXPORTED,
|
||||
'entryState.not_exported' => TimesheetQuery::STATE_NOT_EXPORTED
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ExportQuery::class,
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class YesNoType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'value' => true,
|
||||
'false_values' => [null, 0, false],
|
||||
'false_values' => [null, 0, false, 'false'],
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterfa
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-csv');
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-invoice-csv');
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Csv');
|
||||
$writer->save($filename);
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterfa
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-ods');
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-invoice-ods');
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Ods');
|
||||
$writer->save($filename);
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterf
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-xslx');
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-invoice-xlsx');
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
$writer->save($filename);
|
||||
|
||||
|
||||
@@ -11,11 +11,13 @@ namespace App;
|
||||
|
||||
use App\DependencyInjection\AppExtension;
|
||||
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
|
||||
use App\DependencyInjection\Compiler\ExportServiceCompilerPass;
|
||||
use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
|
||||
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
|
||||
use App\Export\RendererInterface as ExportRendererInterface;
|
||||
use App\Invoice\CalculatorInterface as InvoiceCalculator;
|
||||
use App\Invoice\NumberGeneratorInterface;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Invoice\RendererInterface as InvoiceRendererInterface;
|
||||
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
@@ -30,6 +32,7 @@ class Kernel extends BaseKernel
|
||||
|
||||
public const CONFIG_EXTS = '.{php,xml,yaml,yml}';
|
||||
|
||||
public const TAG_EXPORT_RENDERER = 'export.renderer';
|
||||
public const TAG_INVOICE_RENDERER = 'invoice.renderer';
|
||||
public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator';
|
||||
public const TAG_INVOICE_CALCULATOR = 'invoice.calculator';
|
||||
@@ -47,7 +50,8 @@ class Kernel extends BaseKernel
|
||||
protected function build(ContainerBuilder $container)
|
||||
{
|
||||
$container->registerForAutoconfiguration(TimesheetCalculator::class)->addTag('timesheet.calculator');
|
||||
$container->registerForAutoconfiguration(RendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
|
||||
$container->registerForAutoconfiguration(ExportRendererInterface::class)->addTag(self::TAG_EXPORT_RENDERER);
|
||||
$container->registerForAutoconfiguration(InvoiceRendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
|
||||
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
|
||||
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
|
||||
}
|
||||
@@ -80,6 +84,7 @@ class Kernel extends BaseKernel
|
||||
$container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new InvoiceServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new ExportServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
}
|
||||
|
||||
protected function configureRoutes(RouteCollectionBuilder $routes)
|
||||
|
||||
65
src/Migrations/Version20190124004014.php
Normal file
65
src/Migrations/Version20190124004014.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Adds the exported column to the timesheet table
|
||||
*/
|
||||
final class Version20190124004014 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$platform = $this->getPlatform();
|
||||
|
||||
if (!in_array($platform, ['sqlite', 'mysql'])) {
|
||||
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
|
||||
}
|
||||
|
||||
$timesheet = $this->getTableName('timesheet');
|
||||
|
||||
if ($platform === 'sqlite') {
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD COLUMN exported BOOLEAN NOT NULL DEFAULT false');
|
||||
} else {
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD exported TINYINT(1) NOT NULL DEFAULT false');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$platform = $this->getPlatform();
|
||||
|
||||
if (!in_array($platform, ['sqlite', 'mysql'])) {
|
||||
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
|
||||
}
|
||||
|
||||
$timesheet = $this->getTableName('timesheet');
|
||||
|
||||
if ($platform === 'sqlite') {
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet);
|
||||
$this->addSql('DROP TABLE ' . $timesheet);
|
||||
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)');
|
||||
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet);
|
||||
$this->addSql('DROP TABLE __temp__' . $timesheet);
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
|
||||
} else {
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP exported');
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Repository/Query/ExportQuery.php
Normal file
48
src/Repository/Query/ExportQuery.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Repository\Query;
|
||||
|
||||
/**
|
||||
* Can be used for export queries.
|
||||
*/
|
||||
class ExportQuery extends TimesheetQuery
|
||||
{
|
||||
public const TYPE_HTML = 'html';
|
||||
public const TYPE_CSV = 'csv';
|
||||
public const TYPE_PDF = 'pdf';
|
||||
public const TYPE_XLSX = 'xlsx';
|
||||
public const TYPE_ODS = 'ods';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return ExportQuery
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
if (in_array($type, [self::TYPE_PDF, self::TYPE_CSV, self::TYPE_HTML, self::TYPE_XLSX, self::TYPE_ODS])) {
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ class InvoiceQuery extends TimesheetQuery
|
||||
protected $template;
|
||||
|
||||
/**
|
||||
* TODO can this be removed ???
|
||||
*
|
||||
* @var InvoiceTemplate[]
|
||||
*/
|
||||
protected $templates = [];
|
||||
|
||||
@@ -21,6 +21,8 @@ class TimesheetQuery extends ActivityQuery
|
||||
public const STATE_ALL = 1;
|
||||
public const STATE_RUNNING = 2;
|
||||
public const STATE_STOPPED = 3;
|
||||
public const STATE_EXPORTED = 4;
|
||||
public const STATE_NOT_EXPORTED = 5;
|
||||
|
||||
/**
|
||||
* Overwritten for different default order
|
||||
@@ -44,6 +46,10 @@ class TimesheetQuery extends ActivityQuery
|
||||
* @var int
|
||||
*/
|
||||
protected $state = self::STATE_ALL;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $exported = self::STATE_ALL;
|
||||
/**
|
||||
* @var DateRange
|
||||
*/
|
||||
@@ -84,10 +90,10 @@ class TimesheetQuery extends ActivityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param Activity|int $activity
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function setActivity(Activity $activity = null)
|
||||
public function setActivity($activity = null)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
|
||||
@@ -108,7 +114,7 @@ class TimesheetQuery extends ActivityQuery
|
||||
*/
|
||||
public function setState($state)
|
||||
{
|
||||
if (!is_int($state) && $state != (int) $state) {
|
||||
if (!is_int($state) && $state !== (int) $state) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -120,6 +126,32 @@ class TimesheetQuery extends ActivityQuery
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getExported()
|
||||
{
|
||||
return $this->exported;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $exported
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function setExported($exported)
|
||||
{
|
||||
if (!is_int($exported) && $exported !== (int) $exported) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$exported = (int) $exported;
|
||||
if (in_array($exported, [self::STATE_ALL, self::STATE_EXPORTED, self::STATE_NOT_EXPORTED], true)) {
|
||||
$this->exported = $exported;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
|
||||
@@ -338,11 +338,18 @@ class TimesheetRepository extends AbstractRepository
|
||||
->setParameter('end', $query->getEnd());
|
||||
}
|
||||
|
||||
if ($query->getExported() === TimesheetQuery::STATE_EXPORTED) {
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', true);
|
||||
} elseif ($query->getExported() === TimesheetQuery::STATE_NOT_EXPORTED) {
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', false);
|
||||
}
|
||||
|
||||
if (null !== $query->getActivity()) {
|
||||
$qb->andWhere('t.activity = :activity')
|
||||
->setParameter('activity', $query->getActivity());
|
||||
}
|
||||
|
||||
// TODO if activity is an int, this will fail
|
||||
if (null === $query->getActivity() || null === $query->getActivity()->getProject()) {
|
||||
if (null !== $query->getProject()) {
|
||||
$qb->andWhere('t.project = :project')
|
||||
|
||||
@@ -39,6 +39,7 @@ class DateExtensions extends \Twig_Extension
|
||||
return [
|
||||
new TwigFilter('month_name', [$this, 'monthName']),
|
||||
new TwigFilter('date_short', [$this, 'dateShort']),
|
||||
new TwigFilter('date_time', [$this, 'dateTime']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -53,6 +54,17 @@ class DateExtensions extends \Twig_Extension
|
||||
return date_format($date, $format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $date
|
||||
* @return string
|
||||
*/
|
||||
public function dateTime(DateTime $date)
|
||||
{
|
||||
$format = $this->localeSettings->getDateTimeFormat();
|
||||
|
||||
return date_format($date, $format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
* @return string
|
||||
|
||||
@@ -86,6 +86,13 @@ class Extensions extends \Twig_Extension
|
||||
'user' => 'fas fa-user',
|
||||
'visibility' => 'far fa-eye',
|
||||
'settings' => 'fas fa-wrench',
|
||||
'export' => 'fas fa-database',
|
||||
'pdf' => 'fas fa-file-pdf',
|
||||
'csv' => 'fas fa-table',
|
||||
'ods' => 'fas fa-table',
|
||||
'xlsx' => 'fas fa-file-excel',
|
||||
'on' => 'fas fa-toggle-on',
|
||||
'off' => 'fas fa-toggle-off',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,6 +111,17 @@ class LocaleSettings
|
||||
return $this->getConfigByLocaleAndKey('date', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale specific datetime format, which should be used in combination with the twig filter "|date".
|
||||
*
|
||||
* @param null|string $locale
|
||||
* @return string
|
||||
*/
|
||||
public function getDateTimeFormat(?string $locale = null): string
|
||||
{
|
||||
return $this->getConfigByLocaleAndKey('date_time', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format used in the "|duration" twig filter to display a Timesheet duration.
|
||||
*
|
||||
@@ -143,5 +154,4 @@ class LocaleSettings
|
||||
|
||||
return $this->settings[$locale][$key];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class TimesheetVoter extends AbstractVoter
|
||||
public const EXPORT = 'export';
|
||||
public const VIEW_RATE = 'view_rate';
|
||||
public const EDIT_RATE = 'edit_rate';
|
||||
public const EDIT_EXPORT = 'edit_export';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Timesheet)
|
||||
@@ -38,6 +39,7 @@ class TimesheetVoter extends AbstractVoter
|
||||
self::EXPORT,
|
||||
self::VIEW_RATE,
|
||||
self::EDIT_RATE,
|
||||
self::EDIT_EXPORT,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -92,6 +94,7 @@ class TimesheetVoter extends AbstractVoter
|
||||
case self::EDIT:
|
||||
case self::DELETE:
|
||||
case self::EXPORT:
|
||||
case self::EDIT_EXPORT:
|
||||
$permission .= $attribute;
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user