refactored invoice and export screens (#1046)
This commit is contained in:
@@ -17,6 +17,7 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -25,7 +26,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to export timesheet data.
|
||||
*
|
||||
* @Route(path="/export")
|
||||
* @Security("is_granted('view_export')")
|
||||
* @Security("is_granted('create_export')")
|
||||
*/
|
||||
class ExportController extends AbstractController
|
||||
{
|
||||
@@ -54,61 +55,45 @@ class ExportController extends AbstractController
|
||||
$this->dateFactory = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExportQuery
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDefaultQuery()
|
||||
{
|
||||
$begin = $this->dateFactory->createDateTime('first day of this month 00:00:00');
|
||||
$end = $this->dateFactory->createDateTime('last day of this month 23:59:59');
|
||||
|
||||
$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);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="export", methods={"GET"})
|
||||
* @Security("is_granted('view_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
public function indexAction(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
$showPreview = false;
|
||||
$maxItemsPreview = 500;
|
||||
$entries = [];
|
||||
|
||||
$form = $this->getToolbarForm($query, 'GET');
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$entries = $this->getEntries($query);
|
||||
if ($form->isValid()) {
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
$showPreview = true;
|
||||
$query->setPageSize($maxItemsPreview);
|
||||
$entries = $this->getEntries($query);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('export/index.html.twig', [
|
||||
'query' => $query,
|
||||
'entries' => $entries,
|
||||
'form' => $form->createView(),
|
||||
'renderer' => $this->export->getRenderer(),
|
||||
'preview_max' => $maxItemsPreview,
|
||||
'preview_show' => $showPreview,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/data", name="export_data", methods={"POST"})
|
||||
* @Security("is_granted('create_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function export(Request $request)
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
@@ -127,8 +112,29 @@ class ExportController extends AbstractController
|
||||
}
|
||||
|
||||
$entries = $this->getEntries($query);
|
||||
$response = $renderer->render($entries, $query);
|
||||
|
||||
return $renderer->render($entries, $query);
|
||||
if ($query->isMarkAsExported()) {
|
||||
$this->timesheetRepository->setExported($entries);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function getDefaultQuery(): ExportQuery
|
||||
{
|
||||
$begin = $this->dateFactory->createDateTime('first day of this month 00:00:00');
|
||||
$end = $this->dateFactory->createDateTime('last day of this month 23:59:59');
|
||||
|
||||
$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);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,14 +22,16 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage invoices.
|
||||
* Controller used to create invoices and manage invoice templates.
|
||||
*
|
||||
* @Route(path="/invoice")
|
||||
* @Security("is_granted('view_invoice') or is_granted('view_invoice_template')")
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
class InvoiceController extends AbstractController
|
||||
{
|
||||
@@ -58,26 +60,7 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InvoiceQuery
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDefaultQuery()
|
||||
{
|
||||
$begin = $this->dateTimeFactory->createDateTime('first day of this month');
|
||||
$end = $this->dateTimeFactory->createDateTime('last day of this month');
|
||||
|
||||
$query = new InvoiceQuery();
|
||||
$query->setOrder(InvoiceQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="invoice", methods={"GET"})
|
||||
* @Route(path="/", name="invoice", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
@@ -87,9 +70,14 @@ class InvoiceController extends AbstractController
|
||||
public function indexAction(Request $request, TimesheetRepository $repository)
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
if ($this->isGranted('manage_invoice_template')) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
$this->flashWarning('invoice.first_template');
|
||||
}
|
||||
|
||||
$showPreview = false;
|
||||
$maxItemsPreview = 500;
|
||||
$entries = [];
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
@@ -97,8 +85,22 @@ class InvoiceController extends AbstractController
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
if ($this->isGranted('create_invoice')) {
|
||||
if ($form->isValid()) {
|
||||
/** @var SubmitButton $createButton */
|
||||
$createButton = $form->get('create');
|
||||
if ($createButton->isClicked()) {
|
||||
return $this->renderInvoice($query, $repository);
|
||||
}
|
||||
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
$showPreview = true;
|
||||
$query->setPageSize($maxItemsPreview);
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
@@ -106,34 +108,29 @@ class InvoiceController extends AbstractController
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'form' => $form->createView(),
|
||||
'preview_max' => $maxItemsPreview,
|
||||
'preview_show' => $showPreview,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/print", name="invoice_print", methods={"POST"})
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function printAction(Request $request, TimesheetRepository $repository)
|
||||
protected function getDefaultQuery(): InvoiceQuery
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
$begin = $this->dateTimeFactory->createDateTime('first day of this month');
|
||||
$end = $this->dateTimeFactory->createDateTime('last day of this month');
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query, 'POST');
|
||||
$query = new InvoiceQuery();
|
||||
$query->setOrder(InvoiceQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setExported(InvoiceQuery::STATE_NOT_EXPORTED);
|
||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form->handleRequest($request);
|
||||
return $query;
|
||||
}
|
||||
|
||||
if (!$form->isSubmitted() || !$form->isValid()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
protected function renderInvoice(InvoiceQuery $query, TimesheetRepository $repository)
|
||||
{
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
|
||||
@@ -144,23 +141,28 @@ class InvoiceController extends AbstractController
|
||||
|
||||
foreach ($this->service->getRenderer() as $renderer) {
|
||||
if ($renderer->supports($document)) {
|
||||
return $renderer->render($document, $model);
|
||||
$response = $renderer->render($document, $model);
|
||||
if ($query->isMarkAsExported()) {
|
||||
$repository->setExported($entries);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
$this->flashError('Cannot render invoice: ' . $model->getTemplate()->getRenderer() . ' (' . $document->getName() . ')');
|
||||
$this->flashError(
|
||||
sprintf('Cannot render invoice: %s (%s)', $model->getTemplate()->getRenderer(), $document->getName())
|
||||
);
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param TimesheetRepository $repository
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getEntries(InvoiceQuery $query, TimesheetRepository $repository)
|
||||
protected function getEntries(InvoiceQuery $query, TimesheetRepository $repository): iterable
|
||||
{
|
||||
// customer needs to be defined, as we need the currency for the invoice
|
||||
if (null === $query->getCustomer()) {
|
||||
@@ -181,11 +183,11 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param array $entries
|
||||
* @param Timesheet[] $entries
|
||||
* @return InvoiceModel
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function prepareModel(InvoiceQuery $query, array $entries)
|
||||
protected function prepareModel(InvoiceQuery $query, array $entries): InvoiceModel
|
||||
{
|
||||
$model = new InvoiceModel();
|
||||
$model
|
||||
@@ -214,33 +216,23 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template", defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Route(path="/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice_template')")
|
||||
*
|
||||
* @param int $page
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @Route(path="/template", name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function listTemplateAction($page)
|
||||
public function listTemplateAction(): Response
|
||||
{
|
||||
$templates = $this->invoiceRepository->findByQuery(new BaseQuery());
|
||||
|
||||
return $this->render('invoice/templates.html.twig', [
|
||||
'entries' => $templates,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template/{id}/edit", name="admin_invoice_template_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request)
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
return $this->renderTemplateForm($template, $request);
|
||||
}
|
||||
@@ -248,14 +240,9 @@ class InvoiceController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/template/create", name="admin_invoice_template_create", methods={"GET", "POST"})
|
||||
* @Route(path="/template/create/{id}", name="admin_invoice_template_copy", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_invoice_template')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @param InvoiceTemplate|null $copyFrom
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom)
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom): Response
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
$this->flashWarning('invoice.first_template');
|
||||
@@ -281,16 +268,10 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing template.
|
||||
*
|
||||
* @Route(path="/template/{id}/delete", name="admin_invoice_template_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function deleteTemplate(InvoiceTemplate $template, Request $request)
|
||||
public function deleteTemplate(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
try {
|
||||
$this->invoiceRepository->removeTemplate($template);
|
||||
@@ -299,15 +280,10 @@ class InvoiceController extends AbstractController
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template_paginated', ['page' => $request->get('page')]);
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request)
|
||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
$editForm = $this->createEditForm($template);
|
||||
|
||||
@@ -341,11 +317,7 @@ class InvoiceController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(InvoiceTemplate $template)
|
||||
private function createEditForm(InvoiceTemplate $template): FormInterface
|
||||
{
|
||||
if ($template->getId() === null) {
|
||||
$url = $this->generateUrl('admin_invoice_template_create');
|
||||
|
||||
@@ -84,7 +84,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_export')) {
|
||||
if ($auth->isGranted('create_export')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('export', 'menu.export', 'export', [], $this->getIcon('export'))
|
||||
);
|
||||
|
||||
@@ -12,9 +12,12 @@ namespace App\Export\Renderer;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\DateExtensions;
|
||||
use App\Twig\Extensions;
|
||||
use DateTime;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
@@ -22,72 +25,88 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractSpreadsheetRenderer
|
||||
{
|
||||
public const DATETIME_FORMAT = 'yyyy-mm-dd hh:mm';
|
||||
public const TIME_FORMAT = 'hh:mm';
|
||||
public const DURATION_FORMAT = '[hh]:mm';
|
||||
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
|
||||
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* \(#,##0.00\);_("%1$s"* "-"??_);_(@_)';
|
||||
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
|
||||
|
||||
/**
|
||||
* @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
|
||||
) {
|
||||
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->dateExtension = $dateExtension;
|
||||
$this->extension = $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDateTime(\DateTime $date)
|
||||
protected function setFormattedDateTime(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
return $this->dateExtension->dateShort($date) . ' ' . $this->dateExtension->time($date);
|
||||
}
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
/**
|
||||
* @param int $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;
|
||||
}
|
||||
|
||||
return $timesheet->getUser()->getUsername();
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $seconds
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDuration($seconds)
|
||||
protected function setFormattedTime(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
return $this->extension->duration($seconds);
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||
}
|
||||
|
||||
protected function setFormattedDate(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
||||
}
|
||||
|
||||
protected function setDurationTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
|
||||
$style = $sheet->getStyleByColumnAndRow($column, $row);
|
||||
$style->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
|
||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $duration));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
|
||||
protected function setRateTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
|
||||
}
|
||||
|
||||
protected function setRate(Worksheet $sheet, $column, $row, $rate, $currency)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $rate);
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(
|
||||
sprintf(self::RATE_FORMAT_LEFT, $currency)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,8 +132,11 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$recordsHeaderColumn = 1;
|
||||
$recordsHeaderRow = 1;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.date'));
|
||||
$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.duration'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
$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'));
|
||||
@@ -122,45 +144,67 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$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.tags'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||
foreach ($publicMetaFields as $metaFieldName) {
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaFieldName));
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||
$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;
|
||||
$durationColumn = null;
|
||||
$rateColumn = null;
|
||||
|
||||
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, implode(',', $timesheet->getTagsAsArray()));
|
||||
$this->setFormattedDate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getEnd());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setDuration($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getDuration());
|
||||
$durationColumn = $entryHeaderColumn;
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getRate(), $customerCurrency);
|
||||
$rateColumn = $entryHeaderColumn;
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->getUsername($timesheet));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getActivity()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getDescription());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->translator->trans($exported));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getHourlyRate(), $customerCurrency);
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getFixedRate(), $customerCurrency);
|
||||
$entryHeaderColumn++;
|
||||
|
||||
foreach ($publicMetaFields as $metaFieldName) {
|
||||
$metaField = $timesheet->getMetaField($metaFieldName);
|
||||
$metaFieldValue = '';
|
||||
@@ -169,28 +213,40 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
|
||||
}
|
||||
$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++;
|
||||
}
|
||||
|
||||
$cellDurationTotal = $recordsHeaderColumn - 1;
|
||||
$cellRateTotal = $recordsHeaderColumn;
|
||||
if (null !== $durationColumn) {
|
||||
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
|
||||
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($cellDurationTotal, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
||||
$sheet->setCellValueByColumnAndRow($cellRateTotal, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
||||
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
|
||||
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
if (null !== $rateColumn) {
|
||||
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
|
||||
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
protected function getUsername(Timesheet $timesheet): string
|
||||
{
|
||||
if (!empty($timesheet->getUser()->getAlias())) {
|
||||
return $timesheet->getUser()->getAlias();
|
||||
}
|
||||
|
||||
return $timesheet->getUser()->getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Export\Renderer;
|
||||
use App\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
@@ -72,4 +73,9 @@ final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererI
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s', $duration));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<?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;
|
||||
|
||||
final 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 string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-ods');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
$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';
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@ use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
@@ -48,9 +50,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addUserChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('user', UserType::class, [
|
||||
@@ -59,17 +58,26 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addCustomerChoice(FormBuilderInterface $builder)
|
||||
protected function addUsersChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('users', UserType::class, [
|
||||
'label' => 'label.user',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addCustomerChoice(FormBuilderInterface $builder, bool $required = false)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
$builder->add('customer', HiddenType::class);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($builder) {
|
||||
function (FormEvent $event) use ($builder, $required) {
|
||||
$data = $event->getData();
|
||||
$event->getForm()->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'required' => $required,
|
||||
'project_enabled' => true,
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $data) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
@@ -85,10 +93,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param string $label
|
||||
*/
|
||||
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'label.visible')
|
||||
{
|
||||
$builder->add('visibility', VisibilityType::class, [
|
||||
@@ -98,9 +102,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addPageSizeChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('pageSize', PageSizeType::class, [
|
||||
@@ -108,9 +109,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addUserRoleChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('role', UserRoleType::class, [
|
||||
@@ -118,9 +116,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addDateRangeChoice(FormBuilderInterface $builder, $allowEmpty = true)
|
||||
{
|
||||
$builder->add('daterange', DateRangeType::class, [
|
||||
@@ -129,9 +124,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addProjectChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
@@ -162,9 +154,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addActivityChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
@@ -193,9 +182,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addHiddenPagination(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('page', HiddenType::class, [
|
||||
@@ -203,13 +189,38 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addTagInputField(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('tags', TagsInputType::class, [
|
||||
'required' => false
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addTimesheetStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
'label' => 'label.entryState',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
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
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
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\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -27,13 +27,21 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addExportStateChoice($builder);
|
||||
$this->addUserChoice($builder);
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addUsersChoice($builder);
|
||||
$this->addDateRangeChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addExportType($builder);
|
||||
$this->addTagInputField($builder);
|
||||
$builder->add('markAsExported', CheckboxType::class, [
|
||||
'label' => 'label.mark_as_exported',
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('preview', SubmitType::class, [
|
||||
'label' => 'button.preview',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,23 +52,6 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
$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}
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Type\InvoiceTemplateType;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -25,18 +27,30 @@ class InvoiceToolbarForm extends AbstractToolbarForm
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addTemplateChoice($builder);
|
||||
$this->addUserChoice($builder);
|
||||
$this->addUsersChoice($builder);
|
||||
$this->addDateRangeChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addCustomerChoice($builder, true);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addTagInputField($builder);
|
||||
$this->addExportStateChoice($builder);
|
||||
$builder->add('markAsExported', CheckboxType::class, [
|
||||
'label' => 'label.mark_as_exported',
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('create', SubmitType::class, [
|
||||
'label' => 'button.print',
|
||||
'attr' => ['formtarget' => 'invoice'],
|
||||
]);
|
||||
$builder->add('preview', SubmitType::class, [
|
||||
'label' => 'button.preview',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addTemplateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('template', InvoiceTemplateType::class, [
|
||||
'required' => false,
|
||||
'required' => true,
|
||||
'placeholder' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -37,23 +36,6 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addTimesheetStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
'label' => 'label.entryState',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
@@ -123,6 +123,8 @@ trait RendererTrait
|
||||
'activity.id' => $activity->getId(),
|
||||
'activity.name' => $activity->getName(),
|
||||
'activity.comment' => $activity->getComment(),
|
||||
'activity.fixed_rate' => $activity->getFixedRate(),
|
||||
'activity.hourly_rate' => $activity->getHourlyRate(),
|
||||
]);
|
||||
|
||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||
@@ -138,6 +140,8 @@ trait RendererTrait
|
||||
'project.name' => $project->getName(),
|
||||
'project.comment' => $project->getComment(),
|
||||
'project.order_number' => $project->getOrderNumber(),
|
||||
'project.fixed_rate' => $project->getFixedRate(),
|
||||
'project.hourly_rate' => $project->getHourlyRate(),
|
||||
]);
|
||||
|
||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||
@@ -158,6 +162,8 @@ trait RendererTrait
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
'customer.comment' => $customer->getComment(),
|
||||
'customer.fixed_rate' => $customer->getFixedRate(),
|
||||
'customer.hourly_rate' => $customer->getHourlyRate(),
|
||||
]);
|
||||
|
||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||
|
||||
@@ -50,7 +50,7 @@ class BaseQuery
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
private $currentUser;
|
||||
/**
|
||||
* @var Team[]
|
||||
*/
|
||||
@@ -73,7 +73,7 @@ class BaseQuery
|
||||
|
||||
public function getCurrentUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
return $this->currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +82,7 @@ class BaseQuery
|
||||
*/
|
||||
public function setCurrentUser(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->currentUser = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -15,23 +15,32 @@ class ExportQuery extends TimesheetQuery
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @var bool
|
||||
*/
|
||||
private $markAsExported = false;
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return ExportQuery
|
||||
*/
|
||||
public function setType(string $type)
|
||||
public function setType(string $type): ExportQuery
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMarkAsExported(): bool
|
||||
{
|
||||
return $this->markAsExported;
|
||||
}
|
||||
|
||||
public function setMarkAsExported(bool $markAsExported): ExportQuery
|
||||
{
|
||||
$this->markAsExported = $markAsExported;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,32 +11,38 @@ namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
|
||||
/**
|
||||
* Can be used for invoice queries.
|
||||
*/
|
||||
class InvoiceQuery extends TimesheetQuery
|
||||
{
|
||||
/**
|
||||
* @var InvoiceTemplate
|
||||
*/
|
||||
private $template;
|
||||
|
||||
/**
|
||||
* @return InvoiceTemplate
|
||||
* @var bool
|
||||
*/
|
||||
public function getTemplate()
|
||||
private $markAsExported = false;
|
||||
|
||||
public function getTemplate(): ?InvoiceTemplate
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return InvoiceQuery
|
||||
*/
|
||||
public function setTemplate($template)
|
||||
public function setTemplate(InvoiceTemplate $template): InvoiceQuery
|
||||
{
|
||||
$this->template = $template;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMarkAsExported(): bool
|
||||
{
|
||||
return $this->markAsExported;
|
||||
}
|
||||
|
||||
public function setMarkAsExported(bool $markAsExported): InvoiceQuery
|
||||
{
|
||||
$this->markAsExported = $markAsExported;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ class TimesheetQuery extends ActivityQuery
|
||||
* @var iterable
|
||||
*/
|
||||
protected $tags = [];
|
||||
/**
|
||||
* @var User[]
|
||||
*/
|
||||
private $users = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -58,6 +62,30 @@ class TimesheetQuery extends ActivityQuery
|
||||
$this->dateRange = new DateRange();
|
||||
}
|
||||
|
||||
public function addUser(User $user): self
|
||||
{
|
||||
$this->users[$user->getId()] = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUser(User $user): self
|
||||
{
|
||||
if (isset($this->users[$user->getId()])) {
|
||||
unset($this->users[$user->getId()]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User[]
|
||||
*/
|
||||
public function getUsers(): array
|
||||
{
|
||||
return array_values($this->users);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|null
|
||||
*/
|
||||
|
||||
@@ -538,7 +538,9 @@ class TimesheetRepository extends EntityRepository
|
||||
$user[] = $query->getUser();
|
||||
}
|
||||
|
||||
if (null === $query->getUser() && null !== $query->getCurrentUser()) {
|
||||
$user = array_merge($user, $query->getUsers());
|
||||
|
||||
if (empty($user) && null !== $query->getCurrentUser()) {
|
||||
$currentUser = $query->getCurrentUser();
|
||||
|
||||
if (!$currentUser->isSuperAdmin() && !$currentUser->isAdmin()) {
|
||||
@@ -678,4 +680,25 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
*/
|
||||
public function setExported(array $timesheets)
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb
|
||||
->update(Timesheet::class, 't')
|
||||
->set('t.exported', ':exported')
|
||||
->where($qb->expr()->in('t.id', ':ids'))
|
||||
->setParameter('exported', true, \PDO::PARAM_BOOL)
|
||||
->setParameter('ids', $timesheets)
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$em->commit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<?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\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check permissions on InvoiceTemplateVote.
|
||||
*/
|
||||
class InvoiceTemplateVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: InvoiceTemplate)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::DELETE
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param InvoiceTemplate $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!($subject instanceof InvoiceTemplate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param InvoiceTemplate $subject
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->hasRolePermission($user, $attribute . '_invoice_template');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user