1 | <?php |
||||
2 | /** |
||||
3 | * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). |
||||
4 | * |
||||
5 | * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) |
||||
6 | * |
||||
7 | * This program is free software: you can redistribute it and/or modify |
||||
8 | * it under the terms of the GNU Affero General Public License as published |
||||
9 | * by the Free Software Foundation, either version 3 of the License, or |
||||
10 | * (at your option) any later version. |
||||
11 | * |
||||
12 | * This program is distributed in the hope that it will be useful, |
||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
15 | * GNU Affero General Public License for more details. |
||||
16 | * |
||||
17 | * You should have received a copy of the GNU Affero General Public License |
||||
18 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
||||
19 | */ |
||||
20 | |||||
21 | declare(strict_types=1); |
||||
22 | |||||
23 | namespace App\Controller; |
||||
24 | |||||
25 | use App\DataTables\LogDataTable; |
||||
26 | use App\Entity\Parts\Category; |
||||
27 | use App\Entity\Parts\Footprint; |
||||
28 | use App\Entity\Parts\Manufacturer; |
||||
29 | use App\Entity\Parts\Part; |
||||
30 | use App\Entity\Parts\PartLot; |
||||
31 | use App\Entity\Parts\Storelocation; |
||||
32 | use App\Entity\Parts\Supplier; |
||||
33 | use App\Entity\PriceInformations\Orderdetail; |
||||
34 | use App\Entity\ProjectSystem\Project; |
||||
35 | use App\Exceptions\AttachmentDownloadException; |
||||
36 | use App\Form\Part\PartBaseType; |
||||
37 | use App\Services\Attachments\AttachmentSubmitHandler; |
||||
38 | use App\Services\Attachments\PartPreviewGenerator; |
||||
39 | use App\Services\LogSystem\EventCommentHelper; |
||||
40 | use App\Services\LogSystem\HistoryHelper; |
||||
41 | use App\Services\LogSystem\TimeTravel; |
||||
42 | use App\Services\Parameters\ParameterExtractor; |
||||
43 | use App\Services\Parts\PartLotWithdrawAddHelper; |
||||
44 | use App\Services\Parts\PricedetailHelper; |
||||
45 | use App\Services\ProjectSystem\ProjectBuildPartHelper; |
||||
46 | use DateTime; |
||||
47 | use Doctrine\ORM\EntityManagerInterface; |
||||
48 | use Exception; |
||||
49 | use Omines\DataTablesBundle\DataTableFactory; |
||||
50 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; |
||||
51 | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
52 | use Symfony\Component\Form\FormInterface; |
||||
53 | use Symfony\Component\HttpFoundation\RedirectResponse; |
||||
54 | use Symfony\Component\HttpFoundation\Request; |
||||
55 | use Symfony\Component\HttpFoundation\Response; |
||||
56 | use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; |
||||
57 | use Symfony\Component\Routing\Annotation\Route; |
||||
58 | use Symfony\Contracts\Translation\TranslatorInterface; |
||||
59 | |||||
60 | /** |
||||
61 | * @Route("/part") |
||||
62 | */ |
||||
63 | class PartController extends AbstractController |
||||
64 | { |
||||
65 | protected PricedetailHelper $pricedetailHelper; |
||||
66 | protected PartPreviewGenerator $partPreviewGenerator; |
||||
67 | protected EventCommentHelper $commentHelper; |
||||
68 | |||||
69 | public function __construct(PricedetailHelper $pricedetailHelper, |
||||
70 | PartPreviewGenerator $partPreviewGenerator, EventCommentHelper $commentHelper) |
||||
71 | { |
||||
72 | $this->pricedetailHelper = $pricedetailHelper; |
||||
73 | $this->partPreviewGenerator = $partPreviewGenerator; |
||||
74 | $this->commentHelper = $commentHelper; |
||||
75 | } |
||||
76 | |||||
77 | /** |
||||
78 | * @Route("/{id}/info/{timestamp}", name="part_info") |
||||
79 | * @Route("/{id}", requirements={"id"="\d+"}) |
||||
80 | * |
||||
81 | * @throws Exception |
||||
82 | */ |
||||
83 | public function show(Part $part, Request $request, TimeTravel $timeTravel, HistoryHelper $historyHelper, |
||||
84 | DataTableFactory $dataTable, ParameterExtractor $parameterExtractor, PartLotWithdrawAddHelper $withdrawAddHelper, ?string $timestamp = null): Response |
||||
85 | { |
||||
86 | $this->denyAccessUnlessGranted('read', $part); |
||||
87 | |||||
88 | $timeTravel_timestamp = null; |
||||
89 | if (null !== $timestamp) { |
||||
90 | $this->denyAccessUnlessGranted('show_history', $part); |
||||
91 | //If the timestamp only contains numbers interpret it as unix timestamp |
||||
92 | if (ctype_digit($timestamp)) { |
||||
93 | $timeTravel_timestamp = new DateTime(); |
||||
94 | $timeTravel_timestamp->setTimestamp((int) $timestamp); |
||||
95 | } else { //Try to parse it via DateTime |
||||
96 | $timeTravel_timestamp = new DateTime($timestamp); |
||||
97 | } |
||||
98 | $timeTravel->revertEntityToTimestamp($part, $timeTravel_timestamp); |
||||
99 | } |
||||
100 | |||||
101 | if ($this->isGranted('show_history', $part)) { |
||||
102 | $table = $dataTable->createFromType(LogDataTable::class, [ |
||||
103 | 'filter_elements' => $historyHelper->getAssociatedElements($part), |
||||
104 | 'mode' => 'element_history', |
||||
105 | ], ['pageLength' => 10]) |
||||
106 | ->handleRequest($request); |
||||
107 | |||||
108 | if ($table->isCallback()) { |
||||
109 | return $table->getResponse(); |
||||
110 | } |
||||
111 | } else { |
||||
112 | $table = null; |
||||
113 | } |
||||
114 | |||||
115 | return $this->render( |
||||
116 | 'parts/info/show_part_info.html.twig', |
||||
117 | [ |
||||
118 | 'part' => $part, |
||||
119 | 'datatable' => $table, |
||||
120 | 'pricedetail_helper' => $this->pricedetailHelper, |
||||
121 | 'pictures' => $this->partPreviewGenerator->getPreviewAttachments($part), |
||||
122 | 'timeTravel' => $timeTravel_timestamp, |
||||
123 | 'description_params' => $parameterExtractor->extractParameters($part->getDescription()), |
||||
124 | 'comment_params' => $parameterExtractor->extractParameters($part->getComment()), |
||||
125 | 'withdraw_add_helper' => $withdrawAddHelper, |
||||
126 | ] |
||||
127 | ); |
||||
128 | } |
||||
129 | |||||
130 | /** |
||||
131 | * @Route("/{id}/edit", name="part_edit") |
||||
132 | */ |
||||
133 | public function edit(Part $part, Request $request, EntityManagerInterface $em, TranslatorInterface $translator, |
||||
134 | AttachmentSubmitHandler $attachmentSubmitHandler): Response |
||||
135 | { |
||||
136 | $this->denyAccessUnlessGranted('edit', $part); |
||||
137 | |||||
138 | $form = $this->createForm(PartBaseType::class, $part); |
||||
139 | |||||
140 | $form->handleRequest($request); |
||||
141 | if ($form->isSubmitted() && $form->isValid()) { |
||||
142 | //Upload passed files |
||||
143 | $attachments = $form['attachments']; |
||||
144 | foreach ($attachments as $attachment) { |
||||
145 | /** @var FormInterface $attachment */ |
||||
146 | $options = [ |
||||
147 | 'secure_attachment' => $attachment['secureFile']->getData(), |
||||
148 | 'download_url' => $attachment['downloadURL']->getData(), |
||||
149 | ]; |
||||
150 | |||||
151 | try { |
||||
152 | $attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options); |
||||
153 | } catch (AttachmentDownloadException $attachmentDownloadException) { |
||||
154 | $this->addFlash( |
||||
155 | 'error', |
||||
156 | $translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage() |
||||
157 | ); |
||||
158 | } |
||||
159 | } |
||||
160 | |||||
161 | $this->commentHelper->setMessage($form['log_comment']->getData()); |
||||
162 | |||||
163 | $em->persist($part); |
||||
164 | $em->flush(); |
||||
165 | $this->addFlash('success', 'part.edited_flash'); |
||||
166 | |||||
167 | //Redirect to clone page if user wished that... |
||||
168 | //@phpstan-ignore-next-line |
||||
169 | if ('save_and_clone' === $form->getClickedButton()->getName()) { |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
170 | return $this->redirectToRoute('part_clone', ['id' => $part->getID()]); |
||||
171 | } |
||||
172 | //@phpstan-ignore-next-line |
||||
173 | if ('save_and_new' === $form->getClickedButton()->getName()) { |
||||
174 | return $this->redirectToRoute('part_new'); |
||||
175 | } |
||||
176 | |||||
177 | //Reload form, so the SIUnitType entries use the new part unit |
||||
178 | $form = $this->createForm(PartBaseType::class, $part); |
||||
179 | } elseif ($form->isSubmitted() && !$form->isValid()) { |
||||
180 | $this->addFlash('error', 'part.edited_flash.invalid'); |
||||
181 | } |
||||
182 | |||||
183 | return $this->renderForm('parts/edit/edit_part_info.html.twig', |
||||
184 | [ |
||||
185 | 'part' => $part, |
||||
186 | 'form' => $form, |
||||
187 | ]); |
||||
188 | } |
||||
189 | |||||
190 | /** |
||||
191 | * @Route("/{id}/delete", name="part_delete", methods={"DELETE"}) |
||||
192 | */ |
||||
193 | public function delete(Request $request, Part $part, EntityManagerInterface $entityManager): RedirectResponse |
||||
194 | { |
||||
195 | $this->denyAccessUnlessGranted('delete', $part); |
||||
196 | |||||
197 | if ($this->isCsrfTokenValid('delete'.$part->getId(), $request->request->get('_token'))) { |
||||
198 | |||||
199 | $this->commentHelper->setMessage($request->request->get('log_comment', null)); |
||||
200 | |||||
201 | //Remove part |
||||
202 | $entityManager->remove($part); |
||||
203 | |||||
204 | //Flush changes |
||||
205 | $entityManager->flush(); |
||||
206 | |||||
207 | $this->addFlash('success', 'part.deleted'); |
||||
208 | } |
||||
209 | |||||
210 | return $this->redirectToRoute('homepage'); |
||||
211 | } |
||||
212 | |||||
213 | /** |
||||
214 | * @Route("/new", name="part_new") |
||||
215 | * @Route("/{id}/clone", name="part_clone") |
||||
216 | * @Route("/new_build_part/{project_id}", name="part_new_build_part") |
||||
217 | * @ParamConverter("part", options={"id" = "id"}) |
||||
218 | * @ParamConverter("project", options={"id" = "project_id"}) |
||||
219 | */ |
||||
220 | public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, |
||||
221 | AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper, |
||||
222 | ?Part $part = null, ?Project $project = null): Response |
||||
223 | { |
||||
224 | |||||
225 | if ($part) { //Clone part |
||||
226 | $new_part = clone $part; |
||||
227 | } else if ($project) { //Initialize a new part for a build part from the given project |
||||
228 | //Ensure that the project has not already a build part |
||||
229 | if ($project->getBuildPart() !== null) { |
||||
230 | $this->addFlash('error', 'part.new_build_part.error.build_part_already_exists'); |
||||
231 | return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); |
||||
232 | } |
||||
233 | $new_part = $projectBuildPartHelper->getPartInitialization($project); |
||||
234 | } else { //Create an empty part from scratch |
||||
235 | $new_part = new Part(); |
||||
236 | } |
||||
237 | |||||
238 | $this->denyAccessUnlessGranted('create', $new_part); |
||||
239 | |||||
240 | $cid = $request->get('category', null); |
||||
241 | $category = $cid ? $em->find(Category::class, $cid) : null; |
||||
242 | if (null !== $category && null === $new_part->getCategory()) { |
||||
243 | $new_part->setCategory($category); |
||||
244 | $new_part->setDescription($category->getDefaultDescription()); |
||||
245 | $new_part->setComment($category->getDefaultComment()); |
||||
246 | } |
||||
247 | |||||
248 | $fid = $request->get('footprint', null); |
||||
249 | $footprint = $fid ? $em->find(Footprint::class, $fid) : null; |
||||
250 | if (null !== $footprint && null === $new_part->getFootprint()) { |
||||
251 | $new_part->setFootprint($footprint); |
||||
252 | } |
||||
253 | |||||
254 | $mid = $request->get('manufacturer', null); |
||||
255 | $manufacturer = $mid ? $em->find(Manufacturer::class, $mid) : null; |
||||
256 | if (null !== $manufacturer && null === $new_part->getManufacturer()) { |
||||
257 | $new_part->setManufacturer($manufacturer); |
||||
258 | } |
||||
259 | |||||
260 | $store_id = $request->get('storelocation', null); |
||||
261 | $storelocation = $store_id ? $em->find(Storelocation::class, $store_id) : null; |
||||
262 | if (null !== $storelocation && $new_part->getPartLots()->isEmpty()) { |
||||
263 | $partLot = new PartLot(); |
||||
264 | $partLot->setStorageLocation($storelocation); |
||||
265 | $partLot->setInstockUnknown(true); |
||||
266 | $new_part->addPartLot($partLot); |
||||
267 | } |
||||
268 | |||||
269 | $supplier_id = $request->get('supplier', null); |
||||
270 | $supplier = $supplier_id ? $em->find(Supplier::class, $supplier_id) : null; |
||||
271 | if (null !== $supplier && $new_part->getOrderdetails()->isEmpty()) { |
||||
272 | $orderdetail = new Orderdetail(); |
||||
273 | $orderdetail->setSupplier($supplier); |
||||
274 | $new_part->addOrderdetail($orderdetail); |
||||
275 | } |
||||
276 | |||||
277 | $form = $this->createForm(PartBaseType::class, $new_part); |
||||
278 | |||||
279 | $form->handleRequest($request); |
||||
280 | |||||
281 | if ($form->isSubmitted() && $form->isValid()) { |
||||
282 | //Upload passed files |
||||
283 | $attachments = $form['attachments']; |
||||
284 | foreach ($attachments as $attachment) { |
||||
285 | /** @var FormInterface $attachment */ |
||||
286 | $options = [ |
||||
287 | 'secure_attachment' => $attachment['secureFile']->getData(), |
||||
288 | 'download_url' => $attachment['downloadURL']->getData(), |
||||
289 | ]; |
||||
290 | |||||
291 | try { |
||||
292 | $attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options); |
||||
293 | } catch (AttachmentDownloadException $attachmentDownloadException) { |
||||
294 | $this->addFlash( |
||||
295 | 'error', |
||||
296 | $translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage() |
||||
297 | ); |
||||
298 | } |
||||
299 | } |
||||
300 | |||||
301 | $this->commentHelper->setMessage($form['log_comment']->getData()); |
||||
302 | |||||
303 | $em->persist($new_part); |
||||
304 | $em->flush(); |
||||
305 | $this->addFlash('success', 'part.created_flash'); |
||||
306 | |||||
307 | //If a redirect URL was given, redirect there |
||||
308 | if ($request->query->get('_redirect')) { |
||||
309 | return $this->redirect($request->query->get('_redirect')); |
||||
310 | } |
||||
311 | |||||
312 | //Redirect to clone page if user wished that... |
||||
313 | //@phpstan-ignore-next-line |
||||
314 | if ('save_and_clone' === $form->getClickedButton()->getName()) { |
||||
315 | return $this->redirectToRoute('part_clone', ['id' => $new_part->getID()]); |
||||
316 | } |
||||
317 | //@phpstan-ignore-next-line |
||||
318 | if ('save_and_new' === $form->getClickedButton()->getName()) { |
||||
319 | return $this->redirectToRoute('part_new'); |
||||
320 | } |
||||
321 | |||||
322 | return $this->redirectToRoute('part_edit', ['id' => $new_part->getID()]); |
||||
323 | } |
||||
324 | |||||
325 | if ($form->isSubmitted() && !$form->isValid()) { |
||||
326 | $this->addFlash('error', 'part.created_flash.invalid'); |
||||
327 | } |
||||
328 | |||||
329 | return $this->renderForm('parts/edit/new_part.html.twig', |
||||
330 | [ |
||||
331 | 'part' => $new_part, |
||||
332 | 'form' => $form, |
||||
333 | ]); |
||||
334 | } |
||||
335 | |||||
336 | /** |
||||
337 | * @Route("/{id}/add_withdraw", name="part_add_withdraw", methods={"POST"}) |
||||
338 | */ |
||||
339 | public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response |
||||
340 | { |
||||
341 | if ($this->isCsrfTokenValid('part_withraw' . $part->getID(), $request->request->get('_csfr'))) { |
||||
342 | //Retrieve partlot from the request |
||||
343 | $partLot = $em->find(PartLot::class, $request->request->get('lot_id')); |
||||
344 | if($partLot === null) { |
||||
345 | throw new \RuntimeException('Part lot not found!'); |
||||
346 | } |
||||
347 | //Ensure that the partlot belongs to the part |
||||
348 | if($partLot->getPart() !== $part) { |
||||
349 | throw new \RuntimeException("The origin partlot does not belong to the part!"); |
||||
350 | } |
||||
351 | //Try to determine the target lot (used for move actions) |
||||
352 | $targetLot = $em->find(PartLot::class, $request->request->get('target_id')); |
||||
353 | if ($targetLot && $targetLot->getPart() !== $part) { |
||||
354 | throw new \RuntimeException("The target partlot does not belong to the part!"); |
||||
355 | } |
||||
356 | |||||
357 | //Extract the amount and comment from the request |
||||
358 | $amount = (float) $request->request->get('amount'); |
||||
359 | $comment = $request->request->get('comment'); |
||||
360 | $action = $request->request->get('action'); |
||||
361 | |||||
362 | |||||
363 | |||||
364 | switch ($action) { |
||||
365 | case "withdraw": |
||||
366 | case "remove": |
||||
367 | $this->denyAccessUnlessGranted('withdraw', $partLot); |
||||
368 | $withdrawAddHelper->withdraw($partLot, $amount, $comment); |
||||
369 | break; |
||||
370 | case "add": |
||||
371 | $this->denyAccessUnlessGranted('add', $partLot); |
||||
372 | $withdrawAddHelper->add($partLot, $amount, $comment); |
||||
373 | break; |
||||
374 | case "move": |
||||
375 | $this->denyAccessUnlessGranted('move', $partLot); |
||||
376 | $withdrawAddHelper->move($partLot, $targetLot, $amount, $comment); |
||||
0 ignored issues
–
show
It seems like
$targetLot can also be of type null ; however, parameter $target of App\Services\Parts\PartL...thdrawAddHelper::move() does only seem to accept App\Entity\Parts\PartLot , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
377 | break; |
||||
378 | default: |
||||
379 | throw new \RuntimeException("Unknown action!"); |
||||
380 | } |
||||
381 | |||||
382 | //Save the changes to the DB |
||||
383 | $em->flush(); |
||||
384 | $this->addFlash('success', 'part.withdraw.success'); |
||||
385 | |||||
386 | } else { |
||||
387 | $this->addFlash('error', 'CSRF Token invalid!'); |
||||
388 | } |
||||
389 | |||||
390 | //If an redirect was passed, then redirect there |
||||
391 | if($request->request->get('_redirect')) { |
||||
392 | return $this->redirect($request->request->get('_redirect')); |
||||
393 | } |
||||
394 | //Otherwise just redirect to the part page |
||||
395 | return $this->redirectToRoute('part_info', ['id' => $part->getID()]); |
||||
396 | } |
||||
397 | } |
||||
398 |