src/Diplix/KMGBundle/Controller/Accounting/BillingController.php line 402

Open in your IDE?
  1. <?php
  2. namespace Diplix\KMGBundle\Controller\Accounting;
  3. use Diplix\KMGBundle\Controller\BaseController;
  4. use Diplix\KMGBundle\DataTables\DataTablesHelper;
  5. use Diplix\KMGBundle\DataTables\Expr\ExprStub;
  6. use Diplix\KMGBundle\Entity\Accounting\Billing;
  7. use Diplix\KMGBundle\Entity\Accounting\Document;
  8. use Diplix\KMGBundle\Entity\Accounting\Job;
  9. use Diplix\KMGBundle\Entity\Accounting\JobCalcItem;
  10. use Diplix\KMGBundle\Entity\File;
  11. use Diplix\KMGBundle\Entity\Platform\PlatformClient;
  12. use Diplix\KMGBundle\Entity\Role;
  13. use Diplix\KMGBundle\Entity\StandByDispoEntry;
  14. use Diplix\KMGBundle\Form\Accounting\DocumentForm;
  15. use Diplix\KMGBundle\Form\Accounting\NewDocumentForm;
  16. use Diplix\KMGBundle\Form\DefaultDeleteForm;
  17. use Diplix\KMGBundle\Helper\ClientConfigProvider;
  18. use Diplix\KMGBundle\PdfGeneration\InvoicePdf;
  19. use Diplix\KMGBundle\PdfGeneration\ZugferdGenerator;
  20. use Diplix\KMGBundle\Repository\FileRepository;
  21. use Diplix\KMGBundle\Service\MailHelper;
  22. use Diplix\KMGBundle\Util\ExcelExportHelper;
  23. use Doctrine\ORM\AbstractQuery;
  24. use Doctrine\ORM\EntityManagerInterface;
  25. use Doctrine\ORM\EntityRepository;
  26. use Doctrine\ORM\Tools\Pagination\Paginator;
  27. use horstoeko\zugferd\ZugferdDocumentReader;
  28. use horstoeko\zugferdvisualizer\renderer\ZugferdVisualizerDefaultRenderer;
  29. use horstoeko\zugferdvisualizer\ZugferdVisualizer;
  30. use League\Flysystem\FilesystemOperator;
  31. use PHPExcel_Style_Border;
  32. use PHPExcel_Style_Fill;
  33. use Symfony\Component\Form\FormError;
  34. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  35. use Symfony\Component\HttpFoundation\JsonResponse;
  36. use Symfony\Component\HttpFoundation\RedirectResponse;
  37. use Symfony\Component\HttpFoundation\Request;
  38. use Symfony\Component\HttpFoundation\Response;
  39. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  40. use Symfony\Component\HttpFoundation\StreamedResponse;
  41. use Symfony\Component\PropertyAccess\PropertyAccess;
  42. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  43. class BillingController extends BaseController
  44. {
  45.     public static $bold = [
  46.         'font' => array(
  47.             'name' => 'Arial',
  48.             'bold'=> true,
  49.             'italic'    => false,
  50.         )];
  51.     public static $columnSetup =
  52.                 array(  0=>array('fieldName' => 'doc.id''caption' => '#''headStyle' => 'width:16px;''type' =>DataTablesHelper::T_SELECTOR),
  53.                         1=>array('fieldName' => 'doc.type''caption' => 'T','headStyle' => 'width:16px;'),
  54.                         2=>array('fieldName' => 'doc.number','caption'=>'Nummer'),
  55.                         3=>array('fieldName' => 'doc.date' 'caption'=>'Datum'),
  56.                         4=>array('fieldName' => 'doc.accountingMonth''caption'=>'M'),
  57.                         5=>array('fieldName' => 'doc.accountingYear''caption'=>'Y'),
  58.                         6=>array('fieldName' => 'doc.ref''virtual'=>true'caption'=>'Kunde/Mitglied'),
  59.                         7=>array('fieldName' => 'doc.totalNet','caption'=>'Gesamtbetrag'),
  60.                         8=>array('fieldName' => 'doc.status''caption'=>'Status'),
  61.                         9=>array('fieldName'=>'sentToReceiver','type'=>DataTablesHelper::T_CSSICON'caption'=>'V','virtual'=>true),
  62.                         10=>array('fieldName' => '_buttons''caption' => '''virtual' =>true'headStyle' => 'width:96px;''type' =>DataTablesHelper::T_BUTTONS),
  63.                         11=>array('fieldName' => '_status','visible'=>false,'virtual'=>true),
  64.                 );
  65.     protected function getDth()
  66.     {
  67.         return new DataTablesHelper(self::$columnSetup,
  68.                     [
  69.                     'ajaxUrl'       => $this->get('router')->generate('acc-billing-jsonlist'),
  70.                     'ajaxData'      => 'requestData'// js function to call for getting additional req data
  71.                     'defaultSorting'=> array(3=> 'asc',2=> 'asc')
  72.                     ]);
  73.     }
  74.     protected function init()
  75.     {
  76.         $this->ensureUserHasRole(Role::ACCOUNTING);
  77.     }
  78.     public function __construct(
  79.         protected ClientConfigProvider $ccp,
  80.         FileRepository $fileRepository,
  81.         protected MailHelper $mailHelper,
  82.         protected FilesystemOperator $uploadsFilesystem,
  83.         private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
  84.     )
  85.     {
  86.         $this->fileRepo $fileRepository;
  87.     }
  88.     public function indexAction(Request $request$type)
  89.     {
  90.         $this->init();
  91.         $now = (new \DateTime())->modify('-1 month');
  92.         $aM $request->query->get('am',$now->format('m'));
  93.         $aY $request->query->get('ay',$now->format('Y'));
  94.         $now = new \DateTime(sprintf('%4d-%02d-01',$aY,$aM));
  95.         $from = (clone $now)->sub(new \DateInterval('P3M'));
  96.         $until = (clone $now)->add(new \DateInterval('P3M'));
  97.         $dth $this->getDth();
  98.         return $this->render('@DiplixKMG/Accounting/Billing/list.html.twig',[
  99.             'columnSetup' =>$dth->getColumnSetup(),
  100.             'type' => $type,
  101.             'stati' => Document::$statusMap,
  102.             'filterFrom' =>'',//$from->format('Y-m-01'),
  103.             'filterTo' =>'',//$until->format('Y-m-t'),
  104.             'accountingYear' => $aY,
  105.             'accountingMonth'=> $aM
  106.         ]);
  107.     }
  108.     public function jsonListAction(Request $request)
  109.     {
  110.         $this->init();
  111.         $dth $this->getDth();
  112.         $req $request->query->all();
  113.         /** @var EntityRepository $repo */
  114.         $repo $this->managerRegistry->getRepository(Document::class);
  115.         $cb $repo->createQueryBuilder("doc")
  116.                         ->select("doc")
  117.                         ->leftJoin("doc.member","mem")
  118.                         ->leftJoin("doc.customer","customer");
  119.         // process filtering,ordering,paging
  120.         $dth->setQueryBuilder($cb);
  121.         if ($req['type']!=='')
  122.         {
  123.             $dth->addAnd('doc.type = :type',['type'=>$req['type']]);
  124.         }
  125.         if ($req['status']!=='')
  126.         {
  127.             $dth->addAnd('doc.status = :status',['status'=>$req['status']]);
  128.         }
  129.         if ($req['filterFrom']!=='')
  130.         {
  131.             $from = new \DateTime($req['filterFrom']); $from->setTime(0,0,0);
  132.             $cb->andWhere('doc.date >= :from')->setParameter('from',$from);
  133.         }
  134.         if ($req['filterTo']!=='')
  135.         {
  136.             $to = new \DateTime($req['filterTo']); $to->setTime(23,59,59);
  137.             $cb->andWhere('doc.date <= :to')->setParameter('to',$to);
  138.         }
  139.         if ($req['accountingMonth']!=='')
  140.         {
  141.             $cb->andWhere('doc.accountingMonth = :m')->setParameter('m',$req['accountingMonth']);
  142.         }
  143.         if ($req['accountingYear']!=='')
  144.         {
  145.             $cb->andWhere('doc.accountingYear = :y')->setParameter('y',$req['accountingYear']);
  146.         }
  147.         $rawOrder $dth->getRawColumnOrder($req);
  148.         if ($req['type']===Document::TYPE_RECHNUNG)
  149.         {
  150.             $dth->addSimpleFilter(6,$req,$rawOrder,[ExprStub::like("customer.name")],"customer.name",true);
  151.         }
  152.         else
  153.         if ($req['type']===Document::TYPE_GUTSCHRIFT)
  154.         {
  155.             $dth->addSimpleFilter(6,$req,$rawOrder,[ExprStub::like("mem.name")],"mem.name",true);
  156.         }
  157.         $dth->addSimpleFilter(9,$req,$rawOrder,[],"doc.sentToReceiver");
  158.         $dth->processRequest($request);
  159.         // query and compile paged data
  160.         $query $cb->getQuery();
  161.         $query->setHydrationMode(AbstractQuery::HYDRATE_OBJECT);
  162.         $paginator = new Paginator($cb->getQuery(),true);
  163.         $filteredCount $paginator->count();
  164.         $filteredAndPaged = array();
  165.         $dit $paginator->getIterator();
  166.         /** @var Document  $row */
  167.         foreach ($dit as $row)
  168.         {
  169.             $options = [];
  170.             $options[]= array("route"=>"acc-billing-edit""title"=>"Bearbeiten""icon"=>"glyphicon glyphicon-pencil""parameters"=>array("id"=>$row->getId()) );
  171.             $options[]= array("route"=>"acc-billing-new-plain""title"=>"Klonen""icon"=>"glyphicon glyphicon-screenshot""parameters"=>array("type"=>$row->getType(),"cloneFrom"=>$row->getId()) );
  172.             if ($this->hasUserRole(Role::ACCOUNTING_EDIT))
  173.             {
  174.                 $options[]= array("route"=>"acc-billing-delete""title"=>"Löschen""icon"=>"glyphicon glyphicon-trash""parameters"=>array("id"=>$row->getId()) );
  175.             }
  176.             $options[]= array("route"=>"acc-billing-pdf""title"=>"Dokument""icon"=>"glyphicon glyphicon-floppy-save""parameters"=>array("id"=>$row->getId()) );
  177.             $one = array(
  178.                     => $row->getId(),
  179.                     => Document::$typeMap$row->getType()],
  180.                     => $row->getNumber(),
  181.                     => ( $row->getDate() !== null $row->getDate()->format("d.m.Y") : "-" ),
  182.                     => sprintf('%02d',$row->getAccountingMonth()),
  183.                     => sprintf('%4d',$row->getAccountingYear()),
  184.                     6=> trim( ($row->getCustomer()!==null $row->getCustomer()->getName():"") ." ". ($row->getMember()!==null $row->getMember()->getName():"") ),
  185.                     => $row->getTotalNet(),
  186.                     => Document::$statusMap$row->getStatus() ] ,
  187.                     => $row->isSentToReceiver()!==null 0,
  188.                     10 =>$options,
  189.                     11 => $row->getStatus()
  190.             );
  191.             $filteredAndPaged[]= $one;
  192.         }
  193.         // get total unfiltered count
  194.         $query $repo->createQueryBuilder("doc")
  195.                         ->select("COUNT(doc.id)")
  196.                         ->getQuery();
  197.         $res $query->getResult();
  198.         $totalRecords = (int)$res[0][1];
  199.         // compile output
  200.         $output = array(
  201.             "draw" => $req['draw'],
  202.             "recordsTotal"=>$totalRecords,
  203.             "recordsFiltered"=>$filteredCount,
  204.             "data" => $filteredAndPaged
  205.         );
  206.         return $this->getJsonResponse($request,$output);
  207.     }
  208.     public function newDocumentAction(Request $request,$type)
  209.     {
  210.         $this->init();
  211.         $this->ensureUserHasRole(Role::ACCOUNTING_EDIT);
  212.         $audience $type===Document::TYPE_GUTSCHRIFT Billing::TYPE_MEMBER Billing::TYPE_CUSTOMER;
  213.         return $this->redirectToRoute('acc-billing-process',['type'=>$audience,'fromStr'=>'null']);
  214.     }
  215.     public function editByBillingIdAction(Request $request$billingId)
  216.     {
  217.         $this->init();
  218.         $repo $this->managerRegistry->getRepository(Document::class);
  219.         $row $repo->findOneBy(['billing' =>$billingId]);
  220.         return $this->redirectToRoute('acc-billing-edit',['id'=> ($row!==null $row->getId() : -)]);
  221.     }
  222.     protected function newPdf(Document $row, ?ZugferdGenerator $zugferdGenerator null)
  223.     {
  224.         $config $this->ccp;
  225.         $pdfLogo $config->getPdfLogo()!== null ?  '@' stream_get_contents$this->fileRepo->getStream($config->getPdfLogo()) ) : null;
  226.         $pdf = new InvoicePdf("de","",$pdfLogo);
  227.         $pdf->setZugferdGenerator($zugferdGenerator);
  228.         $pdf->setSender(
  229.             $config->getPdfSenderShort(),
  230.             $config->getPdfSenderLong(),
  231.             $config->getPdfFooter()
  232.         );
  233.         $pdf->render($row);
  234.         return $pdf;
  235.     }
  236.     protected function createPdf(Document $row)
  237.     {
  238.         $pdf $this->newPdf($row);
  239.         $pdfData $pdf->Output('temp.pdf','S');
  240.         $F File::fromPsr7StreamOrString($pdfData$row->generateFilename(), $this->uploadsFilesystem ,'pdf');
  241.         $this->managerRegistry->getManager()->persist($F);
  242.         $this->managerRegistry->getManager()->flush();
  243.         $this->addFlash('success','PDF-Datei wurde erzeugt');
  244.         return $F;
  245.     }
  246.     protected function doFaktura(Document $row)
  247.     {
  248.         $row->setStatus(Document::STATUS_OFFEN);
  249.         $row->setTreatedBy($this->getCurrentUser());
  250.         // generate PDF
  251.         if ($row->getPdfFile()===null)
  252.         {
  253.             $F $this->createPdf($row);
  254.             $row->setPdfFile($F);
  255.         }
  256.     }
  257.     public function editAction(Request $request,$id)
  258.     {
  259.         $this->init();
  260.         $repo $this->managerRegistry->getRepository(Document::class);
  261.         /** @var Document $row */
  262.         $row $repo->findOneBy(['id' =>$id]);
  263.         if (!is_object($row))
  264.         {
  265.             $this->addFlash('warning','Ungültige ID oder Zugriff verweigert.');
  266.             return $this->redirectToRoute('acc-billing');
  267.         }
  268.         $form $this->createForm(DocumentForm::class,$row,[]);
  269.         $form->handleRequest($request);
  270.         if ( ($form->isSubmitted()) && ($form->isValid()) )
  271.         {
  272.             $this->ensureUserHasRole(Role::ACCOUNTING_EDIT);
  273.             try {
  274.                 if ($form->has('extraPdf'))
  275.                 {
  276.                     if (!$this->handleFileUpload($form,$row,"extraPdf",File::TYPE_FILE))
  277.                     {
  278.                         throw new \RuntimeException('Upload fehlgeschlagen.');
  279.                     }
  280.                 }
  281.             }
  282.             catch (\Throwable $ex)
  283.             {
  284.                 $this->addFlash('error',$ex->getMessage());
  285.                 return $this->redirectToRoute('acc-billing-edit',['id'=>$row->getId()]);
  286.             }
  287.             $this->addFlash('success','Änderungen gespeichert.');
  288.             $row->setTotalNet(    $row->recalculateExtraItems() + ($row->getBilling()!==null ?  $row->getBilling()->recalculateTotal() : 0));
  289.             if ($form->has('set_paid') && $form->get('set_paid')->isClicked())
  290.             {
  291.                 $row->setStatus(Document::STATUS_BEZAHLT);
  292.                 $this->addFlash('success','Dokument wurde bezahlt.');
  293.             }
  294.             else
  295.             if ($form->has('set_open') && $form->get('set_open')->isClicked())
  296.             {
  297.                 $this->doFaktura($row);
  298.                 $this->addFlash('success','Dokument wurde auf den Status "offen" aktualisiert.');
  299.             }
  300.             else
  301.             if ($form->has('set_canceled') && $form->get('set_canceled')->isClicked())
  302.             {
  303.                 try
  304.                 {
  305.                     $repo->cancelDocument($row);
  306.                     $this->addFlash('success','Dokument storniert');
  307.                 }
  308.                 catch (\Throwable $ex)
  309.                 {
  310.                     $this->addFlash('danger','Stornieren fehlgeschlagen. '.$ex->getMessage());
  311.                 }
  312.             }
  313.             else
  314.             if ($form->has('set_mahnung') && $form->get('set_mahnung')->isClicked())
  315.             {
  316.                 switch ($row->getStatus())
  317.                 {
  318.                     case Document::STATUS_OFFEN:
  319.                         $row->setStatus(Document::STATUS_MAHNSTUFE1);
  320.                         $row->setReminderPdf$this->createPdf($row) );
  321.                         break;
  322.                     case Document::STATUS_MAHNSTUFE1:
  323.                         $row->setStatus(Document::STATUS_MAHNSTUFE2);
  324.                         $row->setMonition1Pdf$this->createPdf($row) );
  325.                         break;
  326.                     case Document::STATUS_MAHNSTUFE2:
  327.                         $row->setStatus(Document::STATUS_MAHNSTUFE3);
  328.                         $row->setMonition2Pdf$this->createPdf($row) );
  329.                         break;
  330.                     default:
  331.                         throw $this->createAccessDeniedException('Ungültige Transition');
  332.                 }
  333.             }
  334.             $repo->flush();
  335.             return $this->redirectToRoute('acc-billing-edit',['id'=>$row->getId()]);
  336.         }
  337.         return $this->render('@DiplixKMG/Accounting/Billing/edit.html.twig',[
  338.             'form' => $form->createView(),
  339.             'statusMap' => Document::$statusMap,
  340.             'typeMap' => Document::$typeMap,
  341.             'row' => $row
  342.         ]);
  343.     }
  344.     public function extraPdfAction(Request $request$id,$type)
  345.     {
  346.         $this->init();
  347.         $repo $this->managerRegistry->getRepository(Document::class);
  348.         /** @var Document $row */
  349.         $row $repo->findOneBy(['id' => $id]);
  350.         if (!is_object($row)) {
  351.             $this->addFlash('warning''Ungültige ID oder Zugriff verweigert.');
  352.             return $this->redirectToRoute('acc-billing', ['type' => $row->getType()]);
  353.         }
  354.         $pa  PropertyAccess::createPropertyAccessor();
  355.         /** @var File $file */
  356.         $file $pa->getValue($row,$type);
  357.         if (!($file instanceof File))
  358.         {
  359.             throw  $this->createAccessDeniedException('Ungültiger Zugriff');
  360.         }
  361.         return $this->fileRepo->getDownloadResponseFor($request,$file->getId());
  362.     }
  363.     public function pdfAction(Request $request$id)
  364.     {
  365.         $this->init();
  366.         $repo $this->managerRegistry->getRepository(Document::class);
  367.         /** @var Document $row */
  368.         $row $repo->findOneBy(['id' =>$id]);
  369.         if (!is_object($row))
  370.         {
  371.             $this->addFlash('warning','Ungültige ID oder Zugriff verweigert.');
  372.             return $this->redirectToRoute('acc-billing',['type'=>""]);
  373.         }
  374.         // serve the existing pdf if there is one, skip only if forced to do so by request param
  375.         if (($row->getPdfFile() !== null) && ($request->get('forceGeneration',0)===0))
  376.         {
  377.             return $this->fileRepo->getDownloadResponseFor($request,$row->getPdfFile()->getId());
  378.         }
  379.         if (($row->getBilling() === null)&&(count($row->getExtraCalculationItems())===0))
  380.         {
  381.             $this->addFlash('warning','PDF kann nicht mehr neu erzeugt werden da die Abrechnungsdaten nicht mehr vorhanden sind und keine weiteren Posten existieren.');
  382.             return $this->redirectToRoute('acc-billing',['type'=>$row->getType()]);
  383.         }
  384.         // neu erzeugen
  385.         $pdfFile $this->getParameter('dx.temp_dir'). $row->generateFilename().date('Y-m-d_H_i_s').'.pdf';
  386.         try {
  387.             $pdf $this->newPdf($row);
  388.             $pdf->Output($pdfFile);
  389.             return $this->getDownloadResponse($pdfFile,basename($pdfFile));
  390.         }
  391.         catch (\Throwable $ex)
  392.         {
  393.             $this->addFlash('danger','PDF-Erzeugung fehlgeschlagen: '.$ex->getMessage().($ex->getPrevious()!==null $ex->getPrevious()->getMessage():""));
  394.             return $this->redirectToRoute('acc-billing-edit',['id'=>$row->getId()]);
  395.         }
  396.     }
  397.     public function xmlAction(Request $requestEntityManagerInterface $em$id)
  398.     {
  399.         $this->init();
  400.         $repo $em->getRepository(Document::class);
  401.         /** @var Document $row */
  402.         $row $repo->findOneBy(['id' =>$id]);
  403.         if (!is_object($row))
  404.         {
  405.             $this->addFlash('warning','Ungültige ID oder Zugriff verweigert.');
  406.             return $this->redirectToRoute('acc-billing',["type"=>""]);
  407.         }
  408.         $zugferd = new ZugferdGenerator();
  409.         $zugferd->setup($row);
  410.         $zugferd->setSellerFromClientConfig($this->ccp);
  411.         $tempPfd $this->newPdf($row,$zugferd); // fill zugferd with line items
  412.         $zugferd->finalize();
  413.         $xml $zugferd->getXmlContent();
  414.         /** @noinspection TypeUnsafeComparisonInspection */
  415.         if ($request->get('visualize',0)==1)
  416.         {
  417.             $doc ZugferdDocumentReader::readAndGuessFromContent($xml);
  418.             $visualizer = new ZugferdVisualizer($doc);
  419.             // $visualizer->setDefaultTemplate();
  420.             $visualizer->setRenderer(new ZugferdVisualizerDefaultRenderer());
  421.             $visualizer->setTemplate(__DIR__ "/../../Resources/views/Accounting/zugferd_visualizer_template.php");
  422.             return new Response($visualizer->renderMarkup());
  423.         }
  424.         $xmlFile $this->getParameter('dx.temp_dir'). $row->generateFilename().date('Y-m-d_H_i_s').'.xml';
  425.         file_put_contents($xmlFile,$xml);
  426.         return $this->getDownloadResponse($xmlFile,basename($xmlFile));
  427.     }
  428.     public function deleteAction(Request $request$id)
  429.     {
  430.         $this->init();
  431.         $this->ensureUserHasRole(Role::ACCOUNTING_EDIT);
  432.         $repo $this->managerRegistry->getRepository(Document::class);
  433.         /** @var Document $row */
  434.         $row $repo->findOneBy(['id' =>$id]);
  435.         if (!is_object($row))
  436.         {
  437.             $this->addFlash('warning','Ungültige ID oder Zugriff verweigert.');
  438.             return $this->redirectToRoute('acc-billing');
  439.         }
  440.         $form null;
  441.         if ($row->getStatus() === Document::STATUS_ENTWURF)
  442.         {
  443.             $form $this->createFormDefaultDeleteForm::class,array('id' =>$id));
  444.             $form->handleRequest($request);
  445.             if ($form->isSubmitted())
  446.             {
  447.                 if ($form->get('commit')->getData() == 1)
  448.                 {
  449.                     $repo->cancelDocument($row);
  450.                     $this->addFlash('success','message.record-deleted');
  451.                 }
  452.                 return $this->redirectToRoute('acc-billing',['type'=>$row->getType()]);
  453.             }
  454.         }
  455.         return $this->render('@DiplixKMG/Accounting/Billing/delete.html.twig',array (
  456.             'row' => $row,
  457.             'form' => $form !== null $form->createView() : null,
  458.         ));
  459.     }
  460.     public function sendByMailAction(Request $request$id)
  461.     {
  462.         $this->ensureUserHasRole(Role::ACCOUNTING_EDIT);
  463.         $type $request->query->get('type','');
  464.         if ($request->getMethod()!==Request::METHOD_POST)
  465.         {
  466.             throw new \RuntimeException('POST required.');
  467.         }
  468.         $row null;
  469.         $acceptsJson in_array('application/json'$request->getAcceptableContentTypes(),false);
  470.         try {
  471.             $this->init();
  472.             $repo $this->managerRegistry->getRepository(Document::class);
  473.             /** @var Document $row */
  474.             $row $repo->findOneBy(['id' =>$id]);
  475.             if (!is_object($row))
  476.             {
  477.                 throw new \RuntimeException('Ungültige ID oder Zugriff verweigert.');
  478.             }
  479.             if ($row->getStatus() < Document::STATUS_OFFEN) {
  480.                 throw new \RuntimeException('Bitte fakturieren Sie das Dokument vor dem PDF-Versand.');
  481.             }
  482.             if ($row->getStatus() >= Document::STATUS_STORNIERT)
  483.             {
  484.                 throw new \RuntimeException('Ein storniertes Dokument kann nicht per eMail versendet werden.');
  485.             }
  486.             $mh $this->mailHelper;
  487.             $mh->setFilesystem($this->uploadsFilesystem);
  488.             if ($row->getType() === Document::TYPE_RECHNUNG)
  489.             {
  490.                 if ($type === '')
  491.                 {
  492.                     $mh->SendInvoiceToCustomerMail($row);
  493.                     $row->setSentToReceiver(new \DateTime());
  494.                 }
  495.                 else
  496.                 if ($type === 'reminderPdf')
  497.                 {
  498.                     $mh->SendInvoiceFollowUpToCustomerMail($rowDocument::STATUS_MAHNSTUFE1);
  499.                     $row->setReminderSentToReceiver(new \DateTime());
  500.                 }
  501.                 else
  502.                 if ($type === 'monition1Pdf')
  503.                 {
  504.                     $mh->SendInvoiceFollowUpToCustomerMail($rowDocument::STATUS_MAHNSTUFE2);
  505.                     $row->setMonition1SentToReceiver(new \DateTime());
  506.                 }
  507.                 else
  508.                 if ($type === 'monition2Pdf')
  509.                 {
  510.                     $mh->SendInvoiceFollowUpToCustomerMail($rowDocument::STATUS_MAHNSTUFE3);
  511.                     $row->setMonition2SentToReceiver(new \DateTime());
  512.                 }
  513.                 else
  514.                 {
  515.                     throw new \Exception('Invalid type');
  516.                 }
  517.             }
  518.             else
  519.             if ($row->getType() === Document::TYPE_GUTSCHRIFT)
  520.             {
  521.                 $mh->SendGutschriftToMemberMail($row);
  522.                 $row->setSentToReceiver(new \DateTime());
  523.             }
  524.             $repo->flush($row);
  525.             if (!$acceptsJson)
  526.             {
  527.                 $this->addFlash('success','Dokument versendet.');
  528.             }
  529.             else
  530.             {
  531.                 return new JsonResponse(['success'=>true,'message'=>'Dokument versendet']);
  532.             }
  533.         }
  534.         catch (\Throwable $ex)
  535.         {
  536.             if (!$acceptsJson)
  537.             {
  538.                 $this->addFlash('warning',$ex->getMessage());
  539.             }
  540.             else
  541.             {
  542.                 return new JsonResponse(['success'=>false,'message'=>$ex->getMessage()]);
  543.             }
  544.         }
  545.         if (!$acceptsJson)
  546.         {
  547.             return $this->redirectToRoute('acc-billing-edit',['id'=>$row->getId()]);
  548.         }
  549.     }
  550.     public function changeStateAction(Request $request$id$newStatus)
  551.     {
  552.         $this->ensureUserHasRole(Role::ACCOUNTING_EDIT);
  553.         $newStatus = (int)$newStatus;
  554.         try {
  555.             if ($request->getMethod() !== Request::METHOD_POST) {
  556.                 throw new \RuntimeException('POST required.');
  557.             }
  558.             $this->init();
  559.             $repo $this->managerRegistry->getRepository(Document::class);
  560.             /** @var Document $row */
  561.             $row $repo->findOneBy(['id' => $id]);
  562.             if (!is_object($row))
  563.             {
  564.                 throw new \RuntimeException('Ungültige ID oder Zugriff verweigert.');
  565.             }
  566.             if ($newStatus !== Document::STATUS_BEZAHLT)
  567.             {
  568.                 throw new \RuntimeException('Nichtunterstützter Statuswechsel (neuer Status ungültig)');
  569.             }
  570.             if (!in_array$row->getStatus() , [Document::STATUS_OFFEN,Document::STATUS_MAHNSTUFE1,Document::STATUS_MAHNSTUFE2,Document::STATUS_MAHNSTUFE3]))
  571.             {
  572.                 throw new \RuntimeException('Nichtunterstützter Statuswechsel (Dokument im falschen Status)');
  573.             }
  574.             $row->setStatus($newStatus);
  575.             $repo->flush($row);
  576.             return new JsonResponse([
  577.                 'success'=>true,
  578.                 'message'=>'Status geändert.']);
  579.         }
  580.         catch (\Throwable $ex)
  581.         {
  582.             return new JsonResponse(['success' => false'message' => $ex->getMessage()]);
  583.         }
  584.     }
  585.     public function setOpenAction(Request $request$id)
  586.     {
  587.         $this->ensureUserHasRole(Role::ACCOUNTING_EDIT);
  588.         if ($request->getMethod() !== Request::METHOD_POST) {
  589.             throw new \RuntimeException('POST required.');
  590.         }
  591.         $row null;
  592.         $acceptsJson in_array('application/json'$request->getAcceptableContentTypes(), false);
  593.         try {
  594.             $this->init();
  595.             $repo $this->managerRegistry->getRepository(Document::class);
  596.             /** @var Document $row */
  597.             $row $repo->findOneBy(['id' => $id]);
  598.             if (!is_object($row)) {
  599.                 throw new \RuntimeException('Ungültige ID oder Zugriff verweigert.');
  600.             }
  601.             if ($row->getStatus() != Document::STATUS_ENTWURF) {
  602.                 throw new \RuntimeException(sprintf('Dokument %s ist nicht im Entwurfsstatus',$row->getNumber()));
  603.             }
  604.             $this->doFaktura($row);
  605.             $repo->flush($row);
  606.             if (!$acceptsJson)
  607.             {
  608.                 $this->addFlash('success','Dokument wurde fakturiert.');
  609.             }
  610.             else
  611.             {
  612.                 $docUrl $this->generateUrl('acc-billing-pdf',['id'=>$row->getId(), UrlGeneratorInterface::ABSOLUTE_URL]);
  613.                 return new JsonResponse([
  614.                     'success'=>true,
  615.                     'message'=>'Dokument fakturiert',
  616.                     'downloadUrl'=>$docUrl]);
  617.             }
  618.         } catch (\Throwable $ex) {
  619.             if (!$acceptsJson) {
  620.                 $this->addFlash('warning'$ex->getMessage());
  621.             } else {
  622.                 return new JsonResponse(['success' => false'message' => $ex->getMessage()]);
  623.             }
  624.         }
  625.         if (!$acceptsJson) {
  626.             return $this->redirectToRoute('acc-billing', []);
  627.         }
  628.     }
  629.     protected static function sumUpCustomerCalcCategories(Job $j)
  630.     {
  631.         $sum = [ JobCalcItem::Extra => 0JobCalcItem::Wartezeit => 0JobCalcItem::Pauschale => [], JobCalcItem::KM => ];
  632.         /** @var JobCalcItem $item **/
  633.         foreach ($j->getCustomerCalculationItems() as $item)
  634.         {
  635.             if (in_array($item->category,[JobCalcItem::ExtraJobCalcItem::Wartezeit]))
  636.             {
  637.                 $sum[$item->category] += $item->totalNet;
  638.             }
  639.             else
  640.                 if ($item->category == JobCalcItem::KM)
  641.                 {
  642.                     $sum[$item->category] += $item->quantity;
  643.                 }
  644.                 else
  645.                 {
  646.                     $sum[JobCalcItem::Pauschale] []= $item->shortCode;
  647.                 }
  648.         }
  649.         $sum[JobCalcItem::Pauschale] = implode(',',$sum[JobCalcItem::Pauschale]);
  650.         return $sum;
  651.     }
  652.     protected function xlsLine(ExcelExportHelper $xhJob $jobJobCalcItem $item null)
  653.     {
  654.         if ($item === null// first line - Job
  655.         {
  656.             $org "";
  657.             $coce $job->getCostCenter();
  658.             $match = ["3R","3G","3D"];
  659.             foreach ($match as $m)
  660.             {
  661.                 if (strpos($coce,$m)===0)
  662.                 {
  663.                     $org $m;
  664.                     $coce \Safe\substr($coce,strlen($m));
  665.                 }
  666.             }
  667.             $details self::sumUpCustomerCalcCategories($job);
  668.             $xh->addRow(
  669.                 [
  670.                     "Bestellung" => $job->getKnownOrder()->getOrderId(),
  671.                     "Zeit" => $job->getOrderTime()->format('d.m.Y')." ".$job->getOrderTime()->format('H:i'),
  672.                     "Org" => $org,
  673.                     "Kostenstelle" => $coce,
  674.                     "Besteller" => $job->getKnownOrder()->getOrdererName(),
  675.                     "Kommentar" => $job->getInfo(),
  676.                     'KM' => [$details[JobCalcItem::KM] , ExcelExportHelper::TYPE_NUMERIC],
  677.                     // Kunden haben i.d.R. keine Pauschalen :: 'P'  => [$details[JobCalcItem::Pauschale], ExcelExportHelper::TYPE_STRING],
  678.                     'WZ(€ netto)' => [round($details[JobCalcItem::Wartezeit],2) , ExcelExportHelper::XTYPE_CURRENCY],
  679.                     'EX(€ netto)' => [round($details[JobCalcItem::Extra],2) , ExcelExportHelper::XTYPE_CURRENCY],
  680.                     ///////
  681.                     'Posten' => '',
  682.                     'Menge' => '',
  683.                     'Einzelpreis'=>'',
  684.                     'MwSt.'=>'',
  685.                     'Summe'=>'',
  686.                 ],
  687.                 self::$bold
  688.             );
  689.             return;
  690.         }
  691.         $xh->addRow([
  692.             "Menge" =>    $item->quantity,
  693.             "Einzelpreis" => [round($item->amount,2),ExcelExportHelper::XTYPE_CURRENCY],
  694.             "MwSt."   =>  sprintf('%d %%',$item->vat*100),
  695.             "Summe" =>  [round($item->totalNet,2),ExcelExportHelper::XTYPE_CURRENCY],
  696.             "Posten"  => $item->name
  697.         ]);
  698.     }
  699.     public function xlsAction(Request $requeststring $tempDir$id)
  700.     {
  701.         $this->init();
  702.         $repo $this->managerRegistry->getRepository(Document::class);
  703.         /** @var Document $row */
  704.         $row $repo->findOneBy(['id' =>$id]);
  705.         if (!is_object($row))
  706.         {
  707.             $this->addFlash('warning','Ungültige ID oder Zugriff verweigert.');
  708.             return $this->redirectToRoute('acc-billing-edit',['id'=>$id]);
  709.         }
  710.         if ($row->getType()!==Document::TYPE_RECHNUNG)
  711.         {
  712.             $this->addFlash('warning','XLS nur für Rechnungen verfügbar.');
  713.             return $this->redirectToRoute('acc-billing-edit',['id'=>$id]);
  714.         }
  715.         if ($row->getBilling() === null)
  716.         {
  717.             $this->addFlash('warning','Es sind keine Fahrtabrechnungsdaten verknüpft !');
  718.             return $this->redirectToRoute('acc-billing-edit',['id'=>$id]);
  719.         }
  720.         $xh = new ExcelExportHelper();
  721.         ExcelExportHelper::$xlsHeadStyle['borders'] = array('bottom' => ['style' => PHPExcel_Style_Border::BORDER_THIN]);
  722.         $sheetTitle sprintf("%s zu %s %s im Leistungsmonat %02d/%4d\n\n",
  723.             "Beiblatt",
  724.             Document::$typeMap[$row->getType()],
  725.             $row->getNumber(),
  726.             $row->getAccountingMonth(),$row->getAccountingYear());
  727.         $xh->setSingleSheetMode('Beiblatt Rechnung '.$row->getNumber());
  728.         $xh->addIndexedRow($sheetTitle,self::$bold);
  729.         $xh->addIndexedRow('');
  730.         $xh->addIndexedRow('');
  731.         //////////////////////////////////////////////////////
  732.         foreach ($row->getBilling()->getJobList() as $j) {
  733.             if ($j->isIgnoreForCustomer()) {
  734.                 continue;
  735.             }
  736.             $items $j->getCalcItemsFor($row->getBilling()->getType());
  737.             $this->xlsLine($xh$j);
  738.             foreach ($items as $it) {
  739.                 $this->xlsLine($xh$j$it);
  740.             }
  741.         }
  742.         //////////////////////////////////////////////////////////////////
  743.         $tempFn $tempDir uniqid("export",true) . ".xlsx";
  744.         $outFn $row->getNumber()."_".date('Y-m-d_H-i-s').'.' .$xh->getExtension();
  745.         $xh->saveTo($tempFn);
  746.         $resp = new BinaryFileResponse$tempFn);
  747.         $resp->setContentDisposition(
  748.             ResponseHeaderBag::DISPOSITION_ATTACHMENT,
  749.             $outFn
  750.         );
  751.         return $resp;
  752.     }
  753.     public function newAction(Request $request$type)
  754.     {
  755.         $this->init();
  756.         $this->ensureUserHasRole(Role::ACCOUNTING_EDIT);
  757.         $em $this->managerRegistry->getManager();
  758.         $dor $em->getRepository(Document::class);
  759.         // default doc
  760.         $D = new Document();
  761.         $D->setAccountingMonth(date('m'));
  762.         $D->setAccountingYear(date('Y'));
  763.         $D->setType(  $type );
  764.         $D->setTotalNet(0);
  765.         $D->setBilling(null);
  766.         $D->setDate( new \DateTime());
  767.         $cloneFrom $request->query->get('cloneFrom'null);
  768.         $clonedFrom null;
  769.         // fetch if copy is wanted
  770.         if (!empty($cloneFrom))
  771.         {
  772.             $clonedFrom $dor->findOneBy(['id' =>$cloneFrom]);
  773.             if (!is_object($clonedFrom))
  774.             {
  775.                 $this->addFlash('warning','Ungültige ID oder Zugriff verweigert.');
  776.                 return $this->redirectToRoute('acc-billing-edit',['id'=>$id]);
  777.             }
  778.             $D = clone $clonedFrom;
  779.         }
  780.         // set user
  781.         $D->setTreatedBy($this->getCurrentUser());
  782.         $form $this->createForm(NewDocumentForm::class,$D,[
  783.             NewDocumentForm::OPT_BDS => $this->hasUserRole(Role::BEREITSCHAFTSDISPO)
  784.         ]);
  785.         $form->handleRequest($request);
  786.         if ($form->isSubmitted())
  787.         {
  788.             if ((($D->getMember()!==null) xor ($D->getCustomer()!==null))!==true)
  789.             {
  790.                 $this->addFlash('warning','Bitte entweder einen Kunden oder ein Mitglied auswählen');
  791.                 $form->get('customer')->addError(new FormError('Auswahl nötig'));
  792.                 $form->get('member')->addError(new FormError('Auswahl nötig'));
  793.             }
  794.             if ($form->isValid())
  795.             {
  796.                 $D->setNumber$dor->getNewDocumentNumber($D->getType(), $D->getDate()));
  797.                 $address='';
  798.                 if ($D->getMember()!==null)
  799.                     $address $D->getMember()->getAddress();
  800.                 elseif ($D->getCustomer()!==null)
  801.                     $address $D->getCustomer()->getInvoiceAddress();
  802.                 $D->setReceiverAddress($address);
  803.                 if ($D->getType()===Document::TYPE_RECHNUNG)
  804.                 {
  805.                     $ht $D->getCustomer()!==null trim($D->getCustomer()->getPdfDefaultHeader()):'';
  806.                     if (empty($ht)) $ht "Sehr geehrte Damen und Herren,\ndie in Ihrem Auftrag durchgeführten Leistungen berechnen wir wie folgt:";
  807.                     $D->setHeaderText($ht);
  808.                     $ft $D->getCustomer()!==null trim($D->getCustomer()->getPdfDefaultFooter()):'';
  809.                     if (empty($ft)) $ft 'Bitte überweisen Sie den Rechnungsbetrag ohne Abzug innerhalb von 14 Tagen. Geben Sie bei Zahlungen bitte Ihre Kundennummer und die Rechnungsnummer an.';
  810.                     $D->setFooterText($ft);
  811.                 }
  812.                 else
  813.                     if ($D->getType()===Document::TYPE_GUTSCHRIFT)
  814.                     {
  815.                         $D->setFooterText(sprintf("Der Gutschriftsbetrag wird auf Ihr bekanntes Konto überwiesen: %s ",$D->getMember()->getAccountingBankAccount()));
  816.                     }
  817.                 if ($form->get('standByDispo')->getViewData() && ($D->getCustomer()!==null))
  818.                 {
  819.                     $jdd = [];
  820.                     $rr $this->managerRegistry->getRepository(StandByDispoEntry::class);
  821.                     /** @var StandByDispoEntry[] $data */
  822.                     $data $rr->findFor($D->getCustomer()->getId(),$D->getAccountingYear(),$D->getAccountingMonth());
  823.                     foreach ($data as $d)
  824.                     {
  825.                         $jdd$d->getDate()->format('Y-m-d') ] = [
  826.                             "time" => $d->getMatrix(),
  827.                             "comment" => $d->getCommentMatrix()
  828.                         ];
  829.                     }
  830.                     $D->setStandByDispoData($jdd);
  831.                     $sum array_sum(
  832.                         array_map(function($el) {
  833.                             return array_sum($el["time"]);
  834.                         },$jdd)
  835.                     );
  836.                     $JC = new JobCalcItem();
  837.                     $JC->shortCode JobCalcItem::Extra;
  838.                     $JC->name sprintf("Bereitschaftsdisposition");
  839.                     $JC->category JobCalcItem::Extra;
  840.                     $JC->quantity 1;
  841.                     $JC->amount =  round($sum 0.5006666,2);
  842.                     $JC->totalNet round($JC->quantity $JC->amount,2);
  843.                     $JC->vat JobCalcItem::defaultVat();
  844.                     $D->setExtraCalculationItems([$JC]);
  845.                     $D->setTotalNet($JC->totalNet);
  846.                 }
  847.                 $em->persist($D);
  848.                 $em->flush();
  849.                 return $this->redirectToRoute("acc-billing-edit",['id'=>$D->getId()]);
  850.             }
  851.             $this->addFlash('warning','Bitte überprüfen Sie Ihre Eingabe');
  852.         }
  853.         return $this->render('@DiplixKMG/Accounting/Billing/new.html.twig',[
  854.             'form'=>$form->createView(),
  855.             'type'=>$type,
  856.             'typeMap'=>Document::$typeMap,
  857.             "clonedFrom" => $clonedFrom
  858.         ]);
  859.     }
  860. }