From 31a20d0692663d4dad8e6678af1540e4a8b01346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 22 Jan 2023 14:13:56 +0100 Subject: [PATCH] Validate ProjectBuildRequest --- src/Controller/ProjectController.php | 2 +- src/Form/ProjectSystem/ProjectBuildType.php | 43 ++++++-- src/Helpers/Projects/ProjectBuildRequest.php | 97 ++++++++++++++++++- .../ValidProjectBuildRequest.php | 36 +++++++ .../ValidProjectBuildRequestValidator.php | 82 ++++++++++++++++ templates/Projects/build/_form.html.twig | 4 +- translations/validators.en.xlf | 12 +++ 7 files changed, 262 insertions(+), 14 deletions(-) create mode 100644 src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequest.php create mode 100644 src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequestValidator.php diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index d8d1fd19..88db3ddf 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -91,7 +91,7 @@ class ProjectController extends AbstractController $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - //TODO + dump($projectBuildRequest); } return $this->renderForm('Projects/build/build.html.twig', [ diff --git a/src/Form/ProjectSystem/ProjectBuildType.php b/src/Form/ProjectSystem/ProjectBuildType.php index 2e4ae602..c56c413d 100644 --- a/src/Form/ProjectSystem/ProjectBuildType.php +++ b/src/Form/ProjectSystem/ProjectBuildType.php @@ -28,6 +28,7 @@ use Symfony\Component\Form\Event\PreSetDataEvent; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class ProjectBuildType extends AbstractType implements DataMapperInterface @@ -58,6 +59,7 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface $form->add('lot_' . $lot->getID(), SIUnitType::class, [ 'label' => false, 'measurement_unit' => $bomEntry->getPart()->getPartUnit(), + 'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()), ]); } } @@ -65,16 +67,41 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface }); } - public function mapDataToForms($viewData, \Traversable $forms) + public function mapDataToForms($data, \Traversable $forms) { + if (!$data instanceof ProjectBuildRequest) { + throw new \RuntimeException('Data must be an instance of ' . ProjectBuildRequest::class); + } + + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + foreach ($forms as $key => $form) { + //Extract the lot id from the form name + $matches = []; + if (preg_match('/^lot_(\d+)$/', $key, $matches)) { + $lot_id = (int) $matches[1]; + $form->setData($data->getLotWithdrawAmount($lot_id)); + } + } + + } + + public function mapFormsToData(\Traversable $forms, &$data) + { + if (!$data instanceof ProjectBuildRequest) { + throw new \RuntimeException('Data must be an instance of ' . ProjectBuildRequest::class); + } + + /** @var FormInterface[] $forms */ $forms = iterator_to_array($forms); - dump($viewData); - dump ($forms); - } - - public function mapFormsToData(\Traversable $forms, &$viewData) - { - // TODO: Implement mapFormsToData() method. + foreach ($forms as $key => $form) { + //Extract the lot id from the form name + $matches = []; + if (preg_match('/^lot_(\d+)$/', $key, $matches)) { + $lot_id = (int) $matches[1]; + $data->setLotWithdrawAmount($lot_id, $form->getData()); + } + } } } \ No newline at end of file diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php index f04ed0da..4097dbad 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -23,13 +23,21 @@ namespace App\Helpers\Projects; use App\Entity\Parts\PartLot; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; -use Doctrine\Common\Collections\Collection; +use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest; +/** + * @ValidProjectBuildRequest() + */ final class ProjectBuildRequest { private Project $project; private int $number_of_builds; + /** + * @var array + */ + private array $withdraw_amounts = []; + /** * @param Project $project The project that should be build * @param int $number_of_builds The number of builds that should be created @@ -38,6 +46,24 @@ final class ProjectBuildRequest { $this->project = $project; $this->number_of_builds = $number_of_builds; + + $this->initializeArray(); + } + + private function initializeArray(): void + { + //Completely reset the array + $this->withdraw_amounts = []; + + //Now create an array for each BOM entry + foreach ($this->getPartBomEntries() as $bom_entry) { + $remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry); + foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) { + //If the lot has instock use it for the build + $this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount()); + $remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]); + } + } } /** @@ -52,9 +78,73 @@ final class ProjectBuildRequest } } + /** + * Returns the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdraw amount should be get + * @return float + */ + public function getLotWithdrawAmount($lot): float + { + if ($lot instanceof PartLot) { + $lot_id = $lot->getID(); + } elseif (is_int($lot)) { + $lot_id = $lot; + } else { + throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!'); + } + + if (! array_key_exists($lot_id, $this->withdraw_amounts)) { + throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!'); + } + + return $this->withdraw_amounts[$lot_id]; + } + + /** + * Sets the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdraw amount should be get + * @param float $amount + * @return $this + */ + public function setLotWithdrawAmount($lot, float $amount): self + { + if ($lot instanceof PartLot) { + $lot_id = $lot->getID(); + } elseif (is_int($lot)) { + $lot_id = $lot; + } else { + throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!'); + } + + $this->withdraw_amounts[$lot_id] = $amount; + + return $this; + } + + /** + * Returns the sum of all withdraw amounts for the given BOM entry. + * @param ProjectBOMEntry $entry + * @return float + */ + public function getWithdrawAmountSum(ProjectBOMEntry $entry): float + { + $this->ensureBOMEntryValid($entry); + + $sum = 0; + foreach ($this->getPartLotsForBOMEntry($entry) as $lot) { + $sum += $this->getLotWithdrawAmount($lot); + } + + if ($entry->getPart() && !$entry->getPart()->useFloatAmount()) { + $sum = round($sum); + } + + return $sum; + } + /** * Returns the number of available lots to take stock from for the given BOM entry. - * @parm ProjectBOMEntry $entry + * @param ProjectBOMEntry $entry * @return PartLot[]|null Returns null if the entry is a non-part BOM entry */ public function getPartLotsForBOMEntry(ProjectBOMEntry $projectBOMEntry): ?array @@ -65,7 +155,8 @@ final class ProjectBuildRequest return null; } - return $projectBOMEntry->getPart()->getPartLots()->toArray(); + //Filter out all lots which have unknown instock + return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray(); } /** diff --git a/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequest.php b/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequest.php new file mode 100644 index 00000000..b0c99947 --- /dev/null +++ b/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequest.php @@ -0,0 +1,36 @@ +. + */ + +namespace App\Validator\Constraints\ProjectSystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that the given ProjectBuildRequest is valid. + * + * @Annotation + */ +class ValidProjectBuildRequest extends Constraint +{ + public function getTargets(): string + { + return self::CLASS_CONSTRAINT; + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequestValidator.php b/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequestValidator.php new file mode 100644 index 00000000..03a9b81f --- /dev/null +++ b/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequestValidator.php @@ -0,0 +1,82 @@ +. + */ + +namespace App\Validator\Constraints\ProjectSystem; + +use App\Entity\Parts\PartLot; +use App\Helpers\Projects\ProjectBuildRequest; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; + +class ValidProjectBuildRequestValidator extends ConstraintValidator +{ + private function buildViolationForLot(PartLot $partLot, string $message): ConstraintViolationBuilderInterface + { + return $this->context->buildViolation($message) + ->atPath('lot_' . $partLot->getID()) + ->setParameter('{{ lot }}', $partLot->getName()); + } + + public function validate($value, Constraint $constraint) + { + if (!$constraint instanceof ValidProjectBuildRequest) { + throw new UnexpectedTypeException($constraint, ValidProjectBuildRequest::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!$value instanceof ProjectBuildRequest) { + throw new UnexpectedTypeException($value, ProjectBuildRequest::class); + } + + foreach ($value->getPartBomEntries() as $bom_entry) { + $withdraw_sum = $value->getWithdrawAmountSum($bom_entry); + $needed_amount = $value->getNeededAmountForBOMEntry($bom_entry); + + foreach ($value->getPartLotsForBOMEntry($bom_entry) as $lot) { + $withdraw_amount = $value->getLotWithdrawAmount($lot); + + if ($withdraw_amount < 0) { + $this->buildViolationForLot($lot, 'validator.project_build.lot_must_not_smaller_0') + ->addViolation(); + } + + if ($withdraw_amount > $lot->getAmount()) { + $this->buildViolationForLot($lot, 'validator.project_build.lot_must_not_bigger_than_stock') + ->addViolation(); + } + + if ($withdraw_sum > $needed_amount) { + $this->buildViolationForLot($lot, 'validator.project_build.lot_bigger_than_needed') + ->addViolation(); + } + + if ($withdraw_sum < $needed_amount) { + $this->buildViolationForLot($lot, 'validator.project_build.lot_smaller_than_needed') + ->addViolation(); + } + } + } + } +} \ No newline at end of file diff --git a/templates/Projects/build/_form.html.twig b/templates/Projects/build/_form.html.twig index 28843e93..a0139643 100644 --- a/templates/Projects/build/_form.html.twig +++ b/templates/Projects/build/_form.html.twig @@ -9,9 +9,8 @@
- + {# #} {#
@@ -43,6 +42,7 @@ {% endif %}
+ {{ form_errors(form["lot_"~lot.id]) }} {{ form_widget(form["lot_"~lot.id]) }}
diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index cc38990f..eee730b7 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -281,5 +281,17 @@ Prices are not allowed on BOM entries associated with a part. Define the price on the part instead. + + + validator.project_build.lot_bigger_than_needed + You have selected more quantity to withdraw than needed! Remove unnecessary quantity. + + + + + validator.project_build.lot_smaller_than_needed + You have selected less quantity to withdraw than needed for the build! Add additional quantity. + +