create multiple invoices at once (#2465)
This commit is contained in:
@@ -19,8 +19,8 @@ use App\Repository\InvoiceTemplateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\SearchTerm;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
@@ -39,10 +39,6 @@ class InvoiceCreateCommand extends Command
|
||||
* @var ServiceInvoice
|
||||
*/
|
||||
private $serviceInvoice;
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $timesheetRepository;
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
@@ -70,7 +66,6 @@ class InvoiceCreateCommand extends Command
|
||||
|
||||
public function __construct(
|
||||
ServiceInvoice $serviceInvoice,
|
||||
TimesheetRepository $timesheetRepository,
|
||||
CustomerRepository $customerRepository,
|
||||
ProjectRepository $projectRepository,
|
||||
InvoiceTemplateRepository $invoiceTemplateRepository,
|
||||
@@ -78,7 +73,6 @@ class InvoiceCreateCommand extends Command
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
) {
|
||||
$this->serviceInvoice = $serviceInvoice;
|
||||
$this->timesheetRepository = $timesheetRepository;
|
||||
$this->customerRepository = $customerRepository;
|
||||
$this->projectRepository = $projectRepository;
|
||||
$this->invoiceTemplateRepository = $invoiceTemplateRepository;
|
||||
@@ -99,7 +93,7 @@ class InvoiceCreateCommand extends Command
|
||||
->addOption('user', null, InputOption::VALUE_REQUIRED, 'The user to be used for generating the invoices')
|
||||
->addOption('start', null, InputOption::VALUE_OPTIONAL, 'Start date (format: 2020-01-01, default: start of the month)', null)
|
||||
->addOption('end', null, InputOption::VALUE_OPTIONAL, 'End date (format: 2020-01-31, default: end of the month)', null)
|
||||
->addOption('timezone', null, InputOption::VALUE_OPTIONAL, 'Timezone for start and end date query', date_default_timezone_get())
|
||||
->addOption('timezone', null, InputOption::VALUE_OPTIONAL, 'Timezone for start and end date query (fallback: users timezone)', null)
|
||||
->addOption('customer', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of customer IDs', null)
|
||||
->addOption('project', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of project IDs', null)
|
||||
->addOption('by-customer', null, InputOption::VALUE_NONE, 'If set, one invoice for each active customer in the given timerange is created')
|
||||
@@ -157,7 +151,13 @@ class InvoiceCreateCommand extends Command
|
||||
return 1;
|
||||
}
|
||||
|
||||
$timezone = new \DateTimeZone($input->getOption('timezone'));
|
||||
$timezone = $input->getOption('timezone');
|
||||
if ($timezone === null) {
|
||||
$timezone = $user->getTimezone();
|
||||
}
|
||||
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
$dateFactory = new DateTimeFactory($timezone);
|
||||
|
||||
if (!empty($input->getOption('start')) && empty($input->getOption('end'))) {
|
||||
$io->error('You need to supply a end date if a start date was given');
|
||||
@@ -191,7 +191,7 @@ class InvoiceCreateCommand extends Command
|
||||
$start = $input->getOption('start');
|
||||
if (!empty($start)) {
|
||||
try {
|
||||
$start = new \DateTime($start, $timezone);
|
||||
$start = $dateFactory->createDateTime($start);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Invalid start date given');
|
||||
|
||||
@@ -199,14 +199,14 @@ class InvoiceCreateCommand extends Command
|
||||
}
|
||||
}
|
||||
if (!$start instanceof \DateTime) {
|
||||
$start = new \DateTime('first day of this month', $timezone);
|
||||
$start = $dateFactory->getStartOfMonth();
|
||||
}
|
||||
$start->setTime(0, 0, 0);
|
||||
|
||||
$end = $input->getOption('end');
|
||||
if (!empty($end)) {
|
||||
try {
|
||||
$end = new \DateTime($end, $timezone);
|
||||
$end = $dateFactory->createDateTime($end);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Invalid end date given');
|
||||
|
||||
@@ -214,7 +214,7 @@ class InvoiceCreateCommand extends Command
|
||||
}
|
||||
}
|
||||
if (!$end instanceof \DateTime) {
|
||||
$end = new \DateTime('last day of this month', $timezone);
|
||||
$end = $dateFactory->getEndOfMonth();
|
||||
}
|
||||
$end->setTime(23, 59, 59);
|
||||
|
||||
@@ -519,7 +519,7 @@ class InvoiceCreateCommand extends Command
|
||||
*/
|
||||
private function getActiveCustomers(InvoiceQuery $invoiceQuery): array
|
||||
{
|
||||
$results = $this->timesheetRepository->getTimesheetsForQuery($invoiceQuery);
|
||||
$results = $this->serviceInvoice->getInvoiceItems($invoiceQuery);
|
||||
|
||||
$customers = [];
|
||||
|
||||
@@ -537,7 +537,7 @@ class InvoiceCreateCommand extends Command
|
||||
*/
|
||||
private function getActiveProjects(InvoiceQuery $invoiceQuery): array
|
||||
{
|
||||
$results = $this->timesheetRepository->getTimesheetsForQuery($invoiceQuery);
|
||||
$results = $this->serviceInvoice->getInvoiceItems($invoiceQuery);
|
||||
|
||||
$projects = [];
|
||||
|
||||
|
||||
@@ -209,10 +209,8 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
$form->submit($submitData, false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$data->resetByFormError($form->getErrors());
|
||||
}
|
||||
|
||||
if ($request->query->has('setDefaultQuery')) {
|
||||
$data->resetByFormError($form->getErrors(true));
|
||||
} elseif ($request->query->has('setDefaultQuery')) {
|
||||
$params = [];
|
||||
foreach ($form->all() as $name => $child) {
|
||||
$params[$name] = $child->getViewData();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Invoice;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Export\Spreadsheet\AnnotatedObjectExporter;
|
||||
@@ -30,7 +31,6 @@ use App\Repository\Query\InvoiceQuery;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -83,45 +83,30 @@ final class InvoiceController extends AbstractController
|
||||
$this->flashWarning('invoice.first_template');
|
||||
}
|
||||
|
||||
$model = null;
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
if ($this->isGranted('create_invoice') && $form->isValid()) {
|
||||
// use the current request locale as fallback, if no translation was configured
|
||||
if (null !== $query->getTemplate() && null === $query->getTemplate()->getLanguage()) {
|
||||
$query->getTemplate()->setLanguage($request->getLocale());
|
||||
}
|
||||
$models = [];
|
||||
$total = 0;
|
||||
$searched = false;
|
||||
|
||||
try {
|
||||
/** @var SubmitButton $createButton */
|
||||
$createButton = $form->get('create');
|
||||
if ($createButton->isClicked()) {
|
||||
return $this->renderInvoice($query);
|
||||
}
|
||||
|
||||
/** @var SubmitButton $printButton */
|
||||
$printButton = $form->get('print');
|
||||
if ($printButton->isClicked()) {
|
||||
return $this->service->renderInvoice($query, $this->dispatcher);
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$this->logException($ex);
|
||||
$this->flashError('action.update.error', ['%reason%' => 'check doctor/logs']);
|
||||
}
|
||||
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
if ($form->isValid() && $this->isGranted('create_invoice')) {
|
||||
if ($request->query->has('createInvoice')) {
|
||||
try {
|
||||
$model = $this->service->createModel($query);
|
||||
$entries = $this->service->findInvoiceItems($query);
|
||||
if (!empty($entries)) {
|
||||
$model->addEntries($entries);
|
||||
}
|
||||
return $this->renderInvoice($query, $request);
|
||||
} catch (Exception $ex) {
|
||||
$this->logException($ex);
|
||||
$this->flashError('action.update.error', ['%reason%' => 'check doctor/logs']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($form->get('template')->getData() !== null) {
|
||||
try {
|
||||
$models = $this->service->createModels($query);
|
||||
$searched = true;
|
||||
} catch (Exception $ex) {
|
||||
$this->logException($ex);
|
||||
$this->flashError($ex->getMessage());
|
||||
@@ -129,14 +114,73 @@ final class InvoiceController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($models as $model) {
|
||||
$total += \count($model->getCalculator()->getEntries());
|
||||
}
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'query' => $query,
|
||||
'model' => $model,
|
||||
'models' => $models,
|
||||
'form' => $form->createView(),
|
||||
'limit_preview' => ($total > 500),
|
||||
'searched' => $searched,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getDefaultQuery(): InvoiceQuery
|
||||
/**
|
||||
* @Route(path="/preview/{customer}/{template}", name="invoice_preview", methods={"GET"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
public function previewAction(Customer $customer, InvoiceTemplate $template, Request $request, SystemConfiguration $configuration): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isValid() && $this->isGranted('create_invoice')) {
|
||||
try {
|
||||
$query->setTemplate($template);
|
||||
$query->setCustomers([$customer]);
|
||||
$model = $this->service->createModel($query);
|
||||
|
||||
return $this->service->renderInvoiceWithModel($model, $this->dispatcher);
|
||||
} catch (Exception $ex) {
|
||||
$this->logException($ex);
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Failed generating invoice preview: ' . $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/save-invoice/{customer}/{template}", name="invoice_create", methods={"GET"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
public function createInvoiceAction(Customer $customer, InvoiceTemplate $template, Request $request, SystemConfiguration $configuration): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query, $configuration->find('invoice.simple_form'));
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isValid() && $this->isGranted('create_invoice')) {
|
||||
$query->setTemplate($template);
|
||||
$query->setCustomers([$customer]);
|
||||
|
||||
return $this->renderInvoice($query, $request);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
private function getDefaultQuery(): InvoiceQuery
|
||||
{
|
||||
$factory = $this->getDateTimeFactory();
|
||||
$begin = $factory->getStartOfMonth();
|
||||
@@ -159,20 +203,23 @@ final class InvoiceController extends AbstractController
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function renderInvoice(InvoiceQuery $query)
|
||||
private function renderInvoice(InvoiceQuery $query, Request $request)
|
||||
{
|
||||
// use the current request locale as fallback, if no translation was configured
|
||||
if (null !== $query->getTemplate() && null === $query->getTemplate()->getLanguage()) {
|
||||
$query->getTemplate()->setLanguage($request->getLocale());
|
||||
}
|
||||
|
||||
try {
|
||||
$invoice = $this->service->createInvoice($query, $this->dispatcher);
|
||||
$invoices = $this->service->createInvoices($query, $this->dispatcher);
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($this->isGranted('history_invoice')) {
|
||||
return $this->redirectToRoute('admin_invoice_list', ['id' => $invoice->getId()]);
|
||||
if (\count($invoices) === 1) {
|
||||
return $this->redirectToRoute('admin_invoice_list', ['id' => $invoices[0]->getId()]);
|
||||
}
|
||||
|
||||
$file = $this->service->getInvoiceFile($invoice);
|
||||
|
||||
return $this->file($file->getRealPath(), $file->getBasename());
|
||||
return $this->redirectToRoute('admin_invoice_list');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
@@ -182,7 +229,6 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/change-status/{id}/{status}", name="admin_invoice_status", methods={"GET"})
|
||||
* @Security("is_granted('history_invoice')")
|
||||
*/
|
||||
public function changeStatusAction(Invoice $invoice, string $status): Response
|
||||
{
|
||||
@@ -198,7 +244,6 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/delete/{id}", name="admin_invoice_delete", methods={"GET"})
|
||||
* @Security("is_granted('history_invoice')")
|
||||
*/
|
||||
public function deleteInvoiceAction(Invoice $invoice): Response
|
||||
{
|
||||
@@ -214,7 +259,6 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/download/{id}", name="admin_invoice_download", methods={"GET"})
|
||||
* @Security("is_granted('history_invoice')")
|
||||
*/
|
||||
public function downloadAction(Invoice $invoice): Response
|
||||
{
|
||||
@@ -231,7 +275,6 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/show/{page}", defaults={"page": 1}, requirements={"page": "[1-9]\d*"}, name="admin_invoice_list", methods={"GET"})
|
||||
* @Security("is_granted('history_invoice')")
|
||||
*/
|
||||
public function showInvoicesAction(Request $request, int $page): Response
|
||||
{
|
||||
@@ -380,10 +423,6 @@ final class InvoiceController extends AbstractController
|
||||
*/
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom): Response
|
||||
{
|
||||
if (!$this->templateRepository->hasTemplate()) {
|
||||
$this->flashWarning('invoice.first_template');
|
||||
}
|
||||
|
||||
$template = new InvoiceTemplate();
|
||||
|
||||
if (null !== $copyFrom) {
|
||||
@@ -410,7 +449,7 @@ final class InvoiceController extends AbstractController
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
}
|
||||
|
||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
||||
private function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
$editForm = $this->createEditForm($template);
|
||||
|
||||
@@ -433,7 +472,7 @@ final class InvoiceController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getToolbarForm(InvoiceQuery $query, bool $simple): FormInterface
|
||||
private function getToolbarForm(InvoiceQuery $query, bool $simple): FormInterface
|
||||
{
|
||||
$form = $simple ? InvoiceToolbarSimpleForm::class : InvoiceToolbarForm::class;
|
||||
|
||||
|
||||
@@ -30,15 +30,13 @@ class InvoiceSubscriber extends AbstractActionsSubscriber
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isGranted('history_invoice')) {
|
||||
if ($invoice->isNew()) {
|
||||
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending'])]);
|
||||
} elseif ($invoice->isPending()) {
|
||||
$event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid'])]);
|
||||
}
|
||||
|
||||
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
|
||||
$event->addDelete($this->path('admin_invoice_delete', ['id' => $invoice->getId()]), false);
|
||||
if ($invoice->isNew()) {
|
||||
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending'])]);
|
||||
} elseif ($invoice->isPending()) {
|
||||
$event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid'])]);
|
||||
}
|
||||
|
||||
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
|
||||
$event->addDelete($this->path('admin_invoice_delete', ['id' => $invoice->getId()]), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ class InvoicesSubscriber extends AbstractActionsSubscriber
|
||||
{
|
||||
$event->addColumnToggle('#modal_invoice');
|
||||
|
||||
if ($this->isGranted('history_invoice')) {
|
||||
$event->addAction('list', ['url' => $this->path('admin_invoice_list')]);
|
||||
}
|
||||
$event->addAction('list', ['url' => $this->path('admin_invoice_list')]);
|
||||
|
||||
if ($this->isGranted('manage_invoice_template')) {
|
||||
$event->addAction('invoice-template', ['url' => $this->path('admin_invoice_template')]);
|
||||
|
||||
@@ -50,6 +50,10 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
|
||||
$extendedOptions = ['class' => 'selectpicker'];
|
||||
|
||||
if ($options['multiple']) {
|
||||
$extendedOptions['size'] = 1;
|
||||
}
|
||||
|
||||
if (false !== $options['width']) {
|
||||
$extendedOptions['data-width'] = $options['width'];
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@@ -28,22 +27,12 @@ class InvoiceToolbarSimpleForm extends AbstractToolbarForm
|
||||
{
|
||||
$this->addTemplateChoice($builder);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addCustomerChoice($builder, ['required' => true, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
|
||||
$this->addCustomerMultiChoice($builder, ['required' => false, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
|
||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], false, true);
|
||||
$builder->add('markAsExported', CheckboxType::class, [
|
||||
'label' => 'label.mark_as_exported',
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('create', SubmitType::class, [
|
||||
'label' => 'action.save',
|
||||
]);
|
||||
$builder->add('print', SubmitType::class, [
|
||||
'label' => 'button.preview_print',
|
||||
'attr' => ['formtarget' => '_blank'],
|
||||
]);
|
||||
$builder->add('preview', SubmitType::class, [
|
||||
'label' => 'button.preview',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addTemplateChoice(FormBuilderInterface $builder)
|
||||
|
||||
@@ -21,21 +21,9 @@ class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
|
||||
$total = $model->getCalculator()->getTotal();
|
||||
$subtotal = $model->getCalculator()->getSubtotal();
|
||||
$formatter = $model->getFormatter();
|
||||
$entries = $model->getCalculator()->getEntries();
|
||||
|
||||
$begin = null;
|
||||
if ($model->getQuery()->getBegin() !== null) {
|
||||
$begin = $model->getQuery()->getBegin();
|
||||
} elseif (!empty($entries)) {
|
||||
$begin = $entries[0];
|
||||
}
|
||||
|
||||
$end = null;
|
||||
if ($model->getQuery()->getEnd() !== null) {
|
||||
$end = $model->getQuery()->getEnd();
|
||||
} elseif (!empty($entries)) {
|
||||
$end = array_keys($entries)[\count($entries) - 1];
|
||||
}
|
||||
$begin = $model->getQuery()->getBegin();
|
||||
$end = $model->getQuery()->getEnd();
|
||||
|
||||
$values = [
|
||||
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
|
||||
|
||||
@@ -90,7 +90,9 @@ final class ServiceInvoice
|
||||
{
|
||||
foreach ($this->getNumberGenerator() as $generator) {
|
||||
if ($generator->getId() === $name) {
|
||||
return $generator;
|
||||
// several models can co-exist at the same time and NumberGeneratorInterface works
|
||||
// with setModel() instead of __construct() - returning the same instance would lead to bugs!
|
||||
return clone $generator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +118,9 @@ final class ServiceInvoice
|
||||
{
|
||||
foreach ($this->getCalculator() as $calculator) {
|
||||
if ($calculator->getId() === $name) {
|
||||
return $calculator;
|
||||
// several models can co-exist at the same time and CalculatorInterface works
|
||||
// with setModel() instead of __construct() - returning the same instance would lead to bugs!
|
||||
return clone $calculator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,50 +262,33 @@ final class ServiceInvoice
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return array<string, InvoiceItemInterface[]>
|
||||
* @return InvoiceItemInterface[]
|
||||
*/
|
||||
private function findInvoiceItemsWithRepository(InvoiceQuery $query): array
|
||||
public function findInvoiceItems(InvoiceQuery $query): array
|
||||
{
|
||||
@trigger_error('Using findInvoiceItems() is deprecated since 1.14 and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
|
||||
// customer needs to be defined, as we need the currency for the invoice
|
||||
if (!$query->hasCustomers()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$factory = $this->getDateTimeFactory($query);
|
||||
|
||||
if (null === $query->getBegin()) {
|
||||
$query->setBegin($factory->getStartOfMonth());
|
||||
}
|
||||
if (null === $query->getEnd()) {
|
||||
$query->setEnd($factory->getEndOfMonth());
|
||||
}
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
|
||||
$repositories = $this->getInvoiceItemRepositories();
|
||||
$items = [];
|
||||
|
||||
foreach ($repositories as $repository) {
|
||||
$items[\get_class($repository)] = $repository->getInvoiceItemsForQuery($query);
|
||||
}
|
||||
|
||||
return $items;
|
||||
return $this->getInvoiceItems($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return InvoiceItemInterface[]
|
||||
*/
|
||||
public function findInvoiceItems(InvoiceQuery $query): array
|
||||
public function getInvoiceItems(InvoiceQuery $query): array
|
||||
{
|
||||
$entries = [];
|
||||
$temp = $this->findInvoiceItemsWithRepository($query);
|
||||
$items = [];
|
||||
|
||||
foreach ($temp as $repo => $items) {
|
||||
$entries = array_merge($entries, $items);
|
||||
foreach ($this->getInvoiceItemRepositories() as $repository) {
|
||||
$items = array_merge($items, $repository->getInvoiceItemsForQuery($query));
|
||||
}
|
||||
|
||||
return $entries;
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function getDateTimeFactory(InvoiceQuery $query): DateTimeFactory
|
||||
@@ -316,29 +303,17 @@ final class ServiceInvoice
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, InvoiceItemInterface[]> $entries
|
||||
* @param InvoiceItemInterface[] $entries
|
||||
*/
|
||||
private function markEntriesAsExported(iterable $entries)
|
||||
private function markEntriesAsExported(array $entries)
|
||||
{
|
||||
$repositories = $this->getInvoiceItemRepositories();
|
||||
|
||||
foreach ($entries as $repo => $items) {
|
||||
foreach ($repositories as $repository) {
|
||||
if (\get_class($repository) === $repo) {
|
||||
$repository->setExported($items);
|
||||
}
|
||||
}
|
||||
foreach ($this->getInvoiceItemRepositories() as $repository) {
|
||||
$repository->setExported($entries);
|
||||
}
|
||||
}
|
||||
|
||||
public function renderInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Response
|
||||
public function renderInvoiceWithModel(InvoiceModel $model, EventDispatcherInterface $dispatcher): Response
|
||||
{
|
||||
$entries = $this->findInvoiceItemsWithRepository($query);
|
||||
$model = $this->createModel($query);
|
||||
foreach ($entries as $repo => $items) {
|
||||
$model->addEntries($items);
|
||||
}
|
||||
|
||||
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
|
||||
if (null === $document) {
|
||||
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
|
||||
@@ -361,20 +336,21 @@ final class ServiceInvoice
|
||||
);
|
||||
}
|
||||
|
||||
public function renderInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Response
|
||||
{
|
||||
$model = $this->createModel($query);
|
||||
|
||||
return $this->renderInvoiceWithModel($model, $dispatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param InvoiceModel $model
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @return Invoice
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Invoice
|
||||
public function createInvoiceFromModel(InvoiceModel $model, EventDispatcherInterface $dispatcher): Invoice
|
||||
{
|
||||
$entries = $this->findInvoiceItemsWithRepository($query);
|
||||
$model = $this->createModel($query);
|
||||
foreach ($entries as $repo => $items) {
|
||||
$model->addEntries($items);
|
||||
}
|
||||
|
||||
$document = $this->getDocumentByName($model->getTemplate()->getRenderer());
|
||||
if (null === $document) {
|
||||
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
|
||||
@@ -386,8 +362,8 @@ final class ServiceInvoice
|
||||
|
||||
$response = $renderer->render($document, $model);
|
||||
|
||||
if ($query->isMarkAsExported()) {
|
||||
$this->markEntriesAsExported($entries);
|
||||
if ($model->getQuery()->isMarkAsExported()) {
|
||||
$this->markEntriesAsExported($model->getEntries());
|
||||
}
|
||||
|
||||
$event = new InvoicePostRenderEvent($model, $document, $renderer, $response);
|
||||
@@ -411,6 +387,37 @@ final class ServiceInvoice
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @return Invoice[]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createInvoices(InvoiceQuery $query, EventDispatcherInterface $dispatcher): array
|
||||
{
|
||||
$invoices = [];
|
||||
|
||||
$models = $this->createModels($query);
|
||||
foreach ($models as $model) {
|
||||
$invoices[] = $this->createInvoiceFromModel($model, $dispatcher);
|
||||
}
|
||||
|
||||
return $invoices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @return Invoice
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createInvoice(InvoiceQuery $query, EventDispatcherInterface $dispatcher): Invoice
|
||||
{
|
||||
$model = $this->createModel($query);
|
||||
|
||||
return $this->createInvoiceFromModel($model, $dispatcher);
|
||||
}
|
||||
|
||||
public function deleteInvoice(Invoice $invoice)
|
||||
{
|
||||
$invoiceDirectory = $this->getInvoicesDirectory();
|
||||
@@ -426,9 +433,23 @@ final class ServiceInvoice
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createModel(InvoiceQuery $query): InvoiceModel
|
||||
{
|
||||
$model = $this->createModelWithoutEntries($query);
|
||||
$model->addEntries($this->getInvoiceItems($query));
|
||||
|
||||
$this->prepareModelQueryDates($model);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
private function createModelWithoutEntries(InvoiceQuery $query): InvoiceModel
|
||||
{
|
||||
$template = $query->getTemplate();
|
||||
|
||||
if (!$query->hasCustomers()) {
|
||||
throw new \Exception('Cannot create invoice model without customer');
|
||||
}
|
||||
|
||||
if (null === $template) {
|
||||
throw new \Exception('Cannot create invoice model without template');
|
||||
}
|
||||
@@ -449,9 +470,7 @@ final class ServiceInvoice
|
||||
$model->setUser($query->getCurrentUser());
|
||||
}
|
||||
|
||||
if ($query->hasCustomers()) {
|
||||
$model->setCustomer($query->getCustomers()[0]);
|
||||
}
|
||||
$model->setCustomer($query->getCustomers()[0]);
|
||||
|
||||
$generator = $this->getNumberGeneratorByName($query->getTemplate()->getNumberGenerator());
|
||||
if (null === $generator) {
|
||||
@@ -468,4 +487,95 @@ final class ServiceInvoice
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
private function prepareModelQueryDates(InvoiceModel $model)
|
||||
{
|
||||
$begin = $model->getQuery()->getBegin();
|
||||
$end = $model->getQuery()->getEnd();
|
||||
|
||||
if ($begin !== null && $end !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (\count($model->getEntries()) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tmpBegin = null;
|
||||
$tmpEnd = null;
|
||||
|
||||
foreach ($model->getEntries() as $entry) {
|
||||
if ($begin === null) {
|
||||
if ($tmpBegin === null) {
|
||||
$tmpBegin = $entry->getBegin();
|
||||
} else {
|
||||
$tmpBegin = min($entry->getBegin(), $tmpBegin);
|
||||
}
|
||||
}
|
||||
|
||||
if ($end === null) {
|
||||
if ($tmpEnd === null) {
|
||||
$tmpEnd = $entry->getEnd();
|
||||
} else {
|
||||
$tmpEnd = max($entry->getEnd(), $tmpEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($begin === null && $tmpBegin !== null) {
|
||||
$model->getQuery()->setBegin($tmpBegin);
|
||||
}
|
||||
|
||||
if ($end === null && $tmpEnd !== null) {
|
||||
$model->getQuery()->setEnd($tmpEnd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return InvoiceModel[]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createModels(InvoiceQuery $query): array
|
||||
{
|
||||
$models = [];
|
||||
$customerEntries = [];
|
||||
$items = $this->getInvoiceItems($query);
|
||||
|
||||
foreach ($items as $entry) {
|
||||
$customer = $entry->getProject()->getCustomer();
|
||||
$id = $customer->getId();
|
||||
if (!\array_key_exists($id, $customerEntries)) {
|
||||
$customerEntries[$id] = [
|
||||
'customer' => $customer,
|
||||
'entries' => [],
|
||||
];
|
||||
}
|
||||
$customerEntries[$id]['entries'][] = $entry;
|
||||
}
|
||||
|
||||
if (empty($customerEntries)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
uasort($customerEntries, function ($a, $b) {
|
||||
return strcmp($a['customer']->getName(), $b['customer']->getName());
|
||||
});
|
||||
|
||||
foreach ($customerEntries as $id => $settings) {
|
||||
if (empty($settings['entries'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$customerQuery = clone $query;
|
||||
$customerQuery->setCustomers([$settings['customer']]);
|
||||
$model = $this->createModelWithoutEntries($customerQuery);
|
||||
$model->addEntries($settings['entries']);
|
||||
$this->prepareModelQueryDates($model);
|
||||
|
||||
$models[] = $model;
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ final class TimesheetInvoiceItemRepository implements InvoiceItemRepositoryInter
|
||||
*/
|
||||
public function getInvoiceItemsForQuery(InvoiceQuery $query): iterable
|
||||
{
|
||||
return $this->repository->getTimesheetsForQuery($query);
|
||||
return $this->repository->getTimesheetsForQuery($query, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,7 @@ use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
@@ -34,7 +35,6 @@ use Doctrine\ORM\QueryBuilder;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* @extends \Doctrine\ORM\EntityRepository<Timesheet>
|
||||
@@ -837,15 +837,15 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
if ($query->isExported()) {
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', true, PDO::PARAM_BOOL);
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', true, Types::BOOLEAN);
|
||||
} elseif ($query->isNotExported()) {
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', false, PDO::PARAM_BOOL);
|
||||
$qb->andWhere('t.exported = :exported')->setParameter('exported', false, Types::BOOLEAN);
|
||||
}
|
||||
|
||||
if ($query->isBillable()) {
|
||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', true, PDO::PARAM_BOOL);
|
||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', true, Types::BOOLEAN);
|
||||
} elseif ($query->isNotBillable()) {
|
||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', false, PDO::PARAM_BOOL);
|
||||
$qb->andWhere('t.billable = :billable')->setParameter('billable', false, Types::BOOLEAN);
|
||||
}
|
||||
|
||||
if (null !== $query->getModifiedAfter()) {
|
||||
@@ -942,7 +942,7 @@ class TimesheetRepository extends EntityRepository
|
||||
->groupBy('a.id', 'p.id')
|
||||
->orderBy('maxid', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->setParameter('visible', true, PDO::PARAM_BOOL)
|
||||
->setParameter('visible', true, Types::BOOLEAN)
|
||||
;
|
||||
|
||||
if (null !== $user) {
|
||||
@@ -974,7 +974,7 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param Timesheet[]|int[] $timesheets
|
||||
*/
|
||||
public function setExported(array $timesheets)
|
||||
{
|
||||
@@ -987,7 +987,7 @@ class TimesheetRepository extends EntityRepository
|
||||
->update(Timesheet::class, 't')
|
||||
->set('t.exported', ':exported')
|
||||
->where($qb->expr()->in('t.id', ':ids'))
|
||||
->setParameter('exported', true, PDO::PARAM_BOOL)
|
||||
->setParameter('exported', true, Types::BOOLEAN)
|
||||
->setParameter('ids', $timesheets)
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
Reference in New Issue
Block a user