diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php index 9710e9be..54cc1abb 100644 --- a/src/Controller/AssemblyController.php +++ b/src/Controller/AssemblyController.php @@ -29,7 +29,6 @@ use App\Entity\Parts\Part; use App\Form\AssemblySystem\AssemblyAddPartsType; use App\Form\AssemblySystem\AssemblyBuildType; use App\Helpers\Assemblies\AssemblyBuildRequest; -use App\Repository\PartRepository; use App\Services\ImportExportSystem\BOMImporter; use App\Services\AssemblySystem\AssemblyBuildHelper; use Doctrine\Common\Collections\ArrayCollection; @@ -52,14 +51,10 @@ use function Symfony\Component\Translation\t; #[Route(path: '/assembly')] class AssemblyController extends AbstractController { - private PartRepository $partRepository; - public function __construct( private readonly DataTableFactory $dataTableFactory, - private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, ) { - $this->partRepository = $this->entityManager->getRepository(Part::class); } #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])] @@ -161,15 +156,14 @@ class AssemblyController extends AbstractController $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - - //Clear existing BOM entries if requested + // Clear existing entries if requested if ($form->get('clear_existing_bom')->getData()) { $assembly->getBomEntries()->clear(); $entityManager->flush(); } try { - $entries = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [ + $importerResult = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [ 'type' => $form->get('type')->getData(), ]); @@ -177,24 +171,17 @@ class AssemblyController extends AbstractController $errors = $validator->validateProperty($assembly, 'bom_entries'); //If no validation errors occured, save the changes and redirect to edit page - if (count ($errors) === 0) { - foreach ($entries as $entry) { - if ($entry instanceof AssemblyBOMEntry && $entry->getPart() !== null) { - $part = $entry->getPart(); - if ($part->getID() === null) { - $this->partRepository->save($part); - } - } - } + if (count ($errors) === 0 && $importerResult->getViolations()->count() === 0) { + $entries = $importerResult->getBomEntries(); $this->addFlash('success', t('assembly.bom_import.flash.success', ['%count%' => count($entries)])); $entityManager->flush(); + return $this->redirectToRoute('assembly_edit', ['id' => $assembly->getID()]); } - //When we get here, there were validation errors + //Show validation errors $this->addFlash('error', t('assembly.bom_import.flash.invalid_entries')); - } catch (\UnexpectedValueException|\RuntimeException|SyntaxError $e) { $this->addFlash('error', t('assembly.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); } @@ -226,7 +213,8 @@ class AssemblyController extends AbstractController 'assembly' => $assembly, 'jsonTemplate' => $jsonTemplate, 'form' => $form, - 'errors' => $errors ?? null, + 'validationErrors' => $errors ?? null, + 'importerErrors' => isset($importerResult) ? $importerResult->getViolations() : null, ]); } diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 47ba90c9..6688780a 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -39,8 +39,9 @@ use League\Csv\Reader; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; -use RuntimeException; +use Symfony\Contracts\Translation\TranslatorInterface; use UnexpectedValueException; +use Symfony\Component\Validator\ConstraintViolation; /** * @see \App\Tests\Services\ImportExportSystem\BOMImporterTest @@ -57,16 +58,21 @@ class BOMImporter 5 => 'Supplier and ref', ]; - private readonly PartRepository $partRepository; + private string $jsonRoot = ''; - private readonly ManufacturerRepository $manufacturerRepository; + private PartRepository $partRepository; - private readonly CategoryRepository $categoryRepository; + private ManufacturerRepository $manufacturerRepository; - private readonly DBElementRepository $assemblyBOMEntryRepository; + private CategoryRepository $categoryRepository; + + private DBElementRepository $assemblyBOMEntryRepository; + + private TranslatorInterface $translator; public function __construct( private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, private readonly LoggerInterface $logger, private readonly BOMValidationService $validationService ) { @@ -74,6 +80,7 @@ class BOMImporter $this->manufacturerRepository = $entityManager->getRepository(Manufacturer::class); $this->categoryRepository = $entityManager->getRepository(Category::class); $this->assemblyBOMEntryRepository = $entityManager->getRepository(AssemblyBOMEntry::class); + $this->translator = $translator; } protected function configureOptions(OptionsResolver $resolver): OptionsResolver @@ -110,20 +117,21 @@ class BOMImporter } /** - * Converts the given file into an array of BOM entries using the given options and save them into the given assembly. + * Converts the given file into an ImporterResult with an array of BOM entries using the given options and save them into the given assembly. * The changes are not saved into the database yet. - * @return AssemblyBOMEntry[] */ - public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): array + public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): ImporterResult { - $bomEntries = $this->fileToBOMEntries($file, $options, AssemblyBOMEntry::class); + $importerResult = $this->fileToImporterResult($file, $options, AssemblyBOMEntry::class); - //Assign the bom_entries to the assembly - foreach ($bomEntries as $bom_entry) { - $assembly->addBomEntry($bom_entry); + if ($importerResult->getViolations()->count() === 0) { + //Assign the bom_entries to the assembly + foreach ($importerResult->getBomEntries() as $bomEntry) { + $assembly->addBomEntry($bomEntry); + } } - return $bomEntries; + return $importerResult; } /** @@ -152,6 +160,14 @@ class BOMImporter }; } + /** + * Converts the given file into an ImporterResult with an array of BOM entries using the given options. + */ + public function fileToImporterResult(File $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + { + return $this->stringToImporterResult($file->getContent(), $options, $objectType); + } + /** * Import string data into an array of BOM entries, which are not yet assigned to a project. * @param string $data The data to import @@ -164,6 +180,24 @@ class BOMImporter $resolver = $this->configureOptions($resolver); $options = $resolver->resolve($options); + return match ($options['type']) { + 'kicad_pcbnew' => $this->parseKiCADPCB($data, $options, $objectType)->getBomEntries(), + default => throw new InvalidArgumentException('Invalid import type!'), + }; + } + + /** + * Import string data into an array of BOM entries, which are not yet assigned to a project. + * @param string $data The data to import + * @param array $options An array of options + * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries + */ + public function stringToImporterResult(string $data, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult + { + $resolver = new OptionsResolver(); + $resolver = $this->configureOptions($resolver); + $options = $resolver->resolve($options); + return match ($options['type']) { 'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType), 'json' => $this->parseJson($data, $options, $objectType), @@ -171,14 +205,14 @@ class BOMImporter }; } - private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): array + private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): ImporterResult { + $result = new ImporterResult(); + $csv = Reader::createFromString($data); $csv->setDelimiter(';'); $csv->setHeaderOffset(0); - $bom_entries = []; - foreach ($csv->getRecords() as $offset => $entry) { //Translate the german field names to english $entry = $this->normalizeColumnNames($entry); @@ -208,10 +242,10 @@ class BOMImporter $bom_entry->setComment($entry['Supplier and ref'] ?? ''); $bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1)); - $bom_entries[] = $bom_entry; + $result->addBomEntry($bom_entry); } - return $bom_entries; + return $result; } /** @@ -271,30 +305,47 @@ class BOMImporter return $this->validationService->validateBOMEntries($mapped_entries, $options); } - private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): array + private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): ImporterResult { - $result = []; + $result = new ImporterResult(); + $this->jsonRoot = 'JSON Import for '.$objectType === ProjectBOMEntry::class ? 'Project' : 'Assembly'; $data = json_decode($data, true); - foreach ($data as $entry) { + foreach ($data as $key => $entry) { // Check quantity if (!isset($entry['quantity'])) { - throw new UnexpectedValueException('quantity missing'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.quantity.required', + "entry[$key].quantity" + )); } - if (!is_float($entry['quantity']) || $entry['quantity'] <= 0) { - throw new UnexpectedValueException('quantity expected as float greater than 0.0'); + + if (isset($entry['quantity']) && (!is_float($entry['quantity']) || $entry['quantity'] <= 0)) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.quantity.float', + "entry[$key].quantity", + $entry['quantity'] + )); } // Check name if (isset($entry['name']) && !is_string($entry['name'])) { - throw new UnexpectedValueException('name of part list entry expected as string'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.string.notEmpty', + "entry[$key].name", + $entry['name'] + )); } // Check if part is assigned with relevant information if (isset($entry['part'])) { if (!is_array($entry['part'])) { - throw new UnexpectedValueException('The property "part" should be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + "entry[$key].part", + $entry['part'] + )); } $partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0; @@ -303,9 +354,12 @@ class BOMImporter $partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== ''; if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) { - throw new UnexpectedValueException( - 'The property "part" must have either assigned: "id" as integer greater than 0, "name", "mpnr", or "ipn" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.subproperties', + "entry[$key].part", + $entry['part'], + ['%propertyString%' => '"id", "name", "mpnr", or "ipn"'] + )); } $part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null; @@ -314,28 +368,71 @@ class BOMImporter $part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null); if ($part === null) { - $part = new Part(); - $part->setName($entry['part']['name']); + $value = sprintf('part.id: %s, part.mpnr: %s, part.ipn: %s, part.name: %s', + isset($entry['part']['id']) ? '' . $entry['part']['id'] . '' : '-', + isset($entry['part']['mpnr']) ? '' . $entry['part']['mpnr'] . '' : '-', + isset($entry['part']['ipn']) ? '' . $entry['part']['ipn'] . '' : '-', + isset($entry['part']['name']) ? '' . $entry['part']['name'] . '' : '-', + ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part", + $entry['part'], + ['%value%' => $value] + )); } - if ($partNameValid && $part->getName() !== trim($entry['part']['name'])) { - throw new RuntimeException(sprintf('Part name does not match exact the given name. Given for import: %s, found part: %s', $entry['part']['name'], $part->getName())); + if ($partNameValid && $part !== null && isset($entry['part']['name']) && $part->getName() !== trim($entry['part']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.name", + $entry['part']['name'], + [ + '%importValue%' => '' . $entry['part']['name'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getName() . '' + ] + )); } - if ($partIpnValid && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { - throw new RuntimeException(sprintf('Part mpnr does not match exact the given mpnr. Given for import: %s, found part: %s', $entry['part']['mpnr'], $part->getManufacturerProductNumber())); + if ($partMpnrValid && $part !== null && isset($entry['part']['mpnr']) && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.mpnr", + $entry['part']['mpnr'], + [ + '%importValue%' => '' . $entry['part']['mpnr'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getManufacturerProductNumber() . '' + ] + )); } - if ($partIpnValid && $part->getIpn() !== trim($entry['part']['ipn'])) { - throw new RuntimeException(sprintf('Part ipn does not match exact the given ipn. Given for import: %s, found part: %s', $entry['part']['ipn'], $part->getIpn())); + if ($partIpnValid && $part !== null && isset($entry['part']['ipn']) && $part->getIpn() !== trim($entry['part']['ipn'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.ipn", + $entry['part']['ipn'], + [ + '%importValue%' => '' . $entry['part']['ipn'] . '', + '%foundId%' => $part->getID(), + '%foundValue%' => '' . $part->getIpn() . '' + ] + )); } // Part: Description check - if (isset($entry['part']['description']) && !is_null($entry['part']['description'])) { + if (isset($entry['part']['description'])) { if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') { - throw new UnexpectedValueException('The property path "part.description" must be a non-empty string if not null'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.string.notEmpty', + 'entry[$key].part.description', + $entry['part']['description'] + )); } } + $partDescription = $entry['part']['description'] ?? ''; // Part: Manufacturer check @@ -343,7 +440,11 @@ class BOMImporter $manufacturerNameValid = false; if (array_key_exists('manufacturer', $entry['part'])) { if (!is_array($entry['part']['manufacturer'])) { - throw new UnexpectedValueException('The property path "part.manufacturer" must be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + 'entry[$key].part.manufacturer', + $entry['part']['manufacturer']) ?? null + ); } $manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0; @@ -351,23 +452,43 @@ class BOMImporter // Stellen sicher, dass mindestens eine Bedingung für manufacturer erfüllt sein muss if (!$manufacturerIdValid && !$manufacturerNameValid) { - throw new UnexpectedValueException( - 'The property "manufacturer" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', + "entry[$key].part.manufacturer", + $entry['part']['manufacturer'], + )); } } $manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null; $manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null); - if ($manufacturer === null) { - throw new RuntimeException( - 'Manufacturer not found' + if (($manufacturerIdValid || $manufacturerNameValid) && $manufacturer === null) { + $value = sprintf( + 'manufacturer.id: %s, manufacturer.name: %s', + isset($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] !== null ? '' . $entry['part']['manufacturer']['id'] . '' : '-', + isset($entry['part']['manufacturer']['name']) && $entry['part']['manufacturer']['name'] !== null ? '' . $entry['part']['manufacturer']['name'] . '' : '-' ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part.manufacturer", + $entry['part']['manufacturer'], + ['%value%' => $value] + )); } - if ($manufacturerNameValid && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { - throw new RuntimeException(sprintf('Manufacturer name does not match exact the given name. Given for import: %s, found manufacturer: %s', $entry['manufacturer']['name'], $manufacturer->getName())); + if ($manufacturerNameValid && $manufacturer !== null && isset($entry['part']['manufacturer']['name']) && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.manufacturer.name", + $entry['part']['manufacturer']['name'], + [ + '%importValue%' => '' . $entry['part']['manufacturer']['name'] . '', + '%foundId%' => $manufacturer->getID(), + '%foundValue%' => '' . $manufacturer->getName() . '' + ] + )); } // Part: Category check @@ -375,49 +496,82 @@ class BOMImporter $categoryNameValid = false; if (array_key_exists('category', $entry['part'])) { if (!is_array($entry['part']['category'])) { - throw new UnexpectedValueException('part.category must be an array'); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.array', + 'entry[$key].part.category', + $entry['part']['category']) ?? null + ); } $categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0; $categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== ''; if (!$categoryIdValid && !$categoryNameValid) { - throw new UnexpectedValueException( - 'The property "category" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string' - ); + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.manufacturerOrCategoryWithSubProperties', + "entry[$key].part.category", + $entry['part']['category'] + )); } } $category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null; $category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null); - if ($category === null) { - throw new RuntimeException( - 'Category not found' + if (($categoryIdValid || $categoryNameValid) && $category === null) { + $value = sprintf( + 'category.id: %s, category.name: %s', + isset($entry['part']['category']['id']) && $entry['part']['category']['id'] !== null ? '' . $entry['part']['category']['id'] . '' : '-', + isset($entry['part']['category']['name']) && $entry['part']['category']['name'] !== null ? '' . $entry['part']['category']['name'] . '' : '-' ); + + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.notFoundFor', + "entry[$key].part.category", + $entry['part']['category'], + ['%value%' => $value] + )); } - if ($categoryNameValid && $category->getName() !== trim($entry['part']['category']['name'])) { - throw new RuntimeException(sprintf('Category name does not match exact the given name. Given for import: %s, found category: %s', $entry['category']['name'], $category->getName())); + if ($categoryNameValid && $category !== null && isset($entry['part']['category']['name']) && $category->getName() !== trim($entry['part']['category']['name'])) { + $result->addViolation($this->buildJsonViolation( + 'validator.bom_importer.json.parameter.noExactMatch', + "entry[$key].part.category.name", + $entry['part']['category']['name'], + [ + '%importValue%' => '' . $entry['part']['category']['name'] . '', + '%foundId%' => $category->getID(), + '%foundValue%' => '' . $category->getName() . '' + ] + )); } - $part->setDescription($partDescription); - $part->setManufacturer($manufacturer); - $part->setCategory($category); - - if ($partMpnrValid) { - $part->setManufacturerProductNumber($entry['part']['mpnr'] ?? ''); + if ($result->getViolations()->count() > 0) { + continue; } - if ($partIpnValid) { - $part->setIpn($entry['part']['ipn'] ?? ''); + + if ($partDescription !== '') { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Beschreibung des Bauteils mit übernehmen. + $part->setDescription($partDescription); + } + + if ($manufacturer !== null && $manufacturer->getID() !== $part->getManufacturerID()) { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe des Hersteller des Bauteils mit übernehmen. + $part->setManufacturer($manufacturer); + } + + if ($category !== null && $category->getID() !== $part->getCategoryID()) { + //Beim Import / Aktualisieren von zugehörigen Bauteilen zu einer Baugruppe die Kategorie des Bauteils mit übernehmen. + $part->setCategory($category); } if ($objectType === AssemblyBOMEntry::class) { $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); if ($bomEntry === null) { - $name = isset($entry['name']) && $entry['name'] !== null ? trim($entry['name']) : ''; - $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); + if (isset($entry['name']) && $entry['name'] !== '') { + $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); + } if ($bomEntry === null) { $bomEntry = new AssemblyBOMEntry(); @@ -431,9 +585,22 @@ class BOMImporter $bomEntry->setName($entry['name'] ?? ''); $bomEntry->setPart($part); - } - $result[] = $bomEntry; + $result->addBomEntry($bomEntry); + } else { + //Eintrag ohne Part-Relation in die Bauteilliste aufnehmen + + if ($objectType === AssemblyBOMEntry::class) { + $bomEntry = new AssemblyBOMEntry(); + } else { + $bomEntry = new ProjectBOMEntry(); + } + + $bomEntry->setQuantity($entry['quantity']); + $bomEntry->setName($entry['name'] ?? ''); + + $result->addBomEntry($bomEntry); + } } return $result; @@ -462,6 +629,18 @@ class BOMImporter return $out; } + private function buildJsonViolation(string $message, string $propertyPath, mixed $invalidValue = null, array $parameters = []): ConstraintViolation + { + return new ConstraintViolation( + message: $this->translator->trans($message, $parameters, 'validators'), + messageTemplate: $message, + parameters: $parameters, + root: $this->jsonRoot, + propertyPath: $propertyPath, + invalidValue: $invalidValue + ); + } + /** * Parse KiCad schematic BOM with flexible field mapping */ diff --git a/src/Services/ImportExportSystem/ImporterResult.php b/src/Services/ImportExportSystem/ImporterResult.php new file mode 100644 index 00000000..4e289d13 --- /dev/null +++ b/src/Services/ImportExportSystem/ImporterResult.php @@ -0,0 +1,60 @@ +bomEntries = $bomEntries; + $this->violations = new ConstraintViolationList(); + } + + /** + * Fügt einen neuen BOM-Eintrag hinzu. + */ + public function addBomEntry(object $bomEntry): void + { + $this->bomEntries[] = $bomEntry; + } + + /** + * Gibt alle BOM-Einträge zurück. + */ + public function getBomEntries(): array + { + return $this->bomEntries; + } + + /** + * Gibt die Liste der Violation zurück. + */ + public function getViolations(): ConstraintViolationList + { + return $this->violations; + } + + /** + * Fügt eine neue `ConstraintViolation` zur Liste hinzu. + */ + public function addViolation(ConstraintViolation $violation): void + { + $this->violations->add($violation); + } + + /** + * Prüft, ob die Liste der Violationen leer ist. + */ + public function hasViolations(): bool + { + return count($this->violations) > 0; + } +} \ No newline at end of file diff --git a/templates/assemblies/import_bom.html.twig b/templates/assemblies/import_bom.html.twig index dc943042..04ff328a 100644 --- a/templates/assemblies/import_bom.html.twig +++ b/templates/assemblies/import_bom.html.twig @@ -3,16 +3,27 @@ {% block title %}{% trans %}assembly.import_bom{% endtrans %}{% endblock %} {% block before_card %} - {% if errors %} + {% if validationErrors or importerErrors %}