Merge remote-tracking branch 'origin/feature/create-assemblies' into feature/all-features

# Conflicts:
#	.env
#	config/parameters.yaml
#	docs/configuration.md
#	migrations/Version20250304081039.php
#	migrations/Version20250304154507.php
#	src/Controller/AdminPages/BaseAdminController.php
#	src/Controller/ProjectController.php
#	src/Controller/TypeaheadController.php
#	src/DataTables/AssemblyBomEntriesDataTable.php
#	src/DataTables/PartsDataTable.php
#	src/Entity/AssemblySystem/AssemblyBOMEntry.php
#	src/Entity/Attachments/Attachment.php
#	src/Entity/Base/AbstractDBElement.php
#	src/Entity/LogSystem/CollectionElementDeleted.php
#	src/Entity/Parameters/AbstractParameter.php
#	src/Form/AssemblySystem/AssemblyBOMEntryType.php
#	src/Helpers/Assemblies/AssemblyPartAggregator.php
#	src/Security/Voter/AttachmentVoter.php
#	src/Services/AssemblySystem/AssemblyBuildHelper.php
#	src/Services/ImportExportSystem/BOMImporter.php
#	src/Services/ImportExportSystem/EntityExporter.php
#	src/Services/Trees/ToolsTreeBuilder.php
#	src/Services/Trees/TreeViewGenerator.php
#	src/Settings/BehaviorSettings/AssemblyBomTableColumns.php
#	src/Settings/BehaviorSettings/TableSettings.php
#	src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php
#	templates/admin/assembly_admin.html.twig
#	templates/assemblies/build/_form.html.twig
#	templates/assemblies/import_bom.html.twig
#	templates/assemblies/info/_info_card.html.twig
#	templates/assemblies/info/info.html.twig
#	templates/components/tree_macros.html.twig
#	templates/form/collection_types_layout_assembly.html.twig
#	translations/messages.cs.xlf
#	translations/messages.da.xlf
#	translations/messages.de.xlf
#	translations/messages.el.xlf
#	translations/messages.en.xlf
#	translations/messages.es.xlf
#	translations/messages.fr.xlf
#	translations/messages.it.xlf
#	translations/messages.ja.xlf
#	translations/messages.nl.xlf
#	translations/messages.pl.xlf
#	translations/messages.ru.xlf
#	translations/messages.zh.xlf
#	translations/validators.cs.xlf
#	translations/validators.en.xlf
This commit is contained in:
Marcel Diegelmann 2025-10-01 06:56:59 +02:00
commit f7ec130010
42 changed files with 1272 additions and 2273 deletions

View file

@ -49,7 +49,6 @@ use Symfony\Component\Validator\ConstraintViolation;
*/
class BOMImporter
{
private const IMPORT_TYPE_JSON = 'json';
private const IMPORT_TYPE_CSV = 'csv';
private const IMPORT_TYPE_KICAD_PCB = 'kicad_pcbnew';
@ -64,29 +63,29 @@ class BOMImporter
5 => 'Supplier and ref',
];
private readonly PartRepository $partRepository;
private readonly ManufacturerRepository $manufacturerRepository;
private readonly CategoryRepository $categoryRepository;
private readonly DBElementRepository $projectBomEntryRepository;
private readonly DBElementRepository $assemblyBomEntryRepository;
private string $jsonRoot = '';
private PartRepository $partRepository;
private ManufacturerRepository $manufacturerRepository;
private CategoryRepository $categoryRepository;
private DBElementRepository $projectBomEntryRepository;
private DBElementRepository $assemblyBomEntryRepository;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly TranslatorInterface $translator,
private readonly LoggerInterface $logger,
private readonly BOMValidationService $validationService
private readonly BOMValidationService $validationService,
private readonly TranslatorInterface $translator
) {
$this->partRepository = $entityManager->getRepository(Part::class);
$this->manufacturerRepository = $entityManager->getRepository(Manufacturer::class);
$this->categoryRepository = $entityManager->getRepository(Category::class);
$this->projectBomEntryRepository = $entityManager->getRepository(ProjectBOMEntry::class);
$this->assemblyBomEntryRepository = $entityManager->getRepository(AssemblyBOMEntry::class);
$this->partRepository = $this->entityManager->getRepository(Part::class);
$this->manufacturerRepository = $this->entityManager->getRepository(Manufacturer::class);
$this->categoryRepository = $this->entityManager->getRepository(Category::class);
$this->projectBomEntryRepository = $this->entityManager->getRepository(ProjectBOMEntry::class);
$this->assemblyBomEntryRepository = $this->entityManager->getRepository(AssemblyBOMEntry::class);
}
protected function configureOptions(OptionsResolver $resolver): OptionsResolver
@ -167,22 +166,6 @@ class BOMImporter
return $this->stringToBOMEntries($importObject, $file->getContent(), $options);
}
/**
* Validate BOM data before importing
* @return array Validation result with errors, warnings, and info
*/
public function validateBOMData(string $data, array $options): array
{
$resolver = new OptionsResolver();
$resolver = $this->configureOptions($resolver);
$options = $resolver->resolve($options);
return match ($options['type']) {
'kicad_pcbnew' => $this->validateKiCADPCB($data),
'kicad_schematic' => $this->validateKiCADSchematicData($data, $options),
default => throw new InvalidArgumentException('Invalid import type!'),
};
}
/**
* Handles the conversion of an uploaded file into an ImporterResult for a given project or assembly.
@ -238,6 +221,23 @@ class BOMImporter
return $this->stringToImporterResult($importObject, $file->getContent(), $options);
}
/**
* Validate BOM data before importing
* @return array Validation result with errors, warnings, and info
*/
public function validateBOMData(string $data, array $options): array
{
$resolver = new OptionsResolver();
$resolver = $this->configureOptions($resolver);
$options = $resolver->resolve($options);
return match ($options['type']) {
'kicad_pcbnew' => $this->validateKiCADPCB($data),
'kicad_schematic' => $this->validateKiCADSchematicData($data, $options),
default => throw new InvalidArgumentException('Invalid import type!'),
};
}
/**
* Import string data into an array of BOM entries, which are not yet assigned to a project.
*
@ -255,7 +255,7 @@ class BOMImporter
return match ($options['type']) {
self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $importObject)->getBomEntries(),
self::IMPORT_TYPE_KICAD_SCHEMATIC => $this->parseKiCADPCB($data, $importObject)->getBomEntries(),
self::IMPORT_TYPE_KICAD_SCHEMATIC => $this->parseKiCADSchematic($data, $options),
default => throw new InvalidArgumentException($this->translator->trans('validator.bom_importer.invalid_import_type', [], 'validators')),
};
}
@ -296,9 +296,8 @@ class BOMImporter
* validates the required fields, and creates BOM entries for each record in the data.
* The BOM entries are added to the provided Project or Assembly, depending on the context.
*
* @param string $data The semicolon- or comma-delimited CSV data to be parsed.
* @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
* @param string $data The semicolon- or comma-delimited CSV data to be parsed
*
* @return ImporterResult The result of the import process, containing the created BOM entries.
*
* @throws UnexpectedValueException If required fields are missing in the provided data.
@ -858,7 +857,7 @@ class BOMImporter
if (isset($entry['name'])) {
$givenName = trim($entry['name']) === '' ? null : trim ($entry['name']);
if ($givenName !== null && $part !== null && $part->getName() !== $givenName) {
if ($givenName !== null && $bomEntry->getPart() !== null && $bomEntry->getPart()->getName() !== $givenName) {
//Apply different names for parts list entry
$bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name']));
}
@ -867,7 +866,11 @@ class BOMImporter
}
if (isset($entry['designator'])) {
$bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator']));
if ($bomEntry instanceof ProjectBOMEntry) {
$bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator']));
} elseif ($bomEntry instanceof AssemblyBOMEntry) {
$bomEntry->setDesignator(trim($entry['designator']) === '' ? '' : trim($entry['designator']));
}
}
$bomEntry->setPart($part);
@ -957,6 +960,7 @@ class BOMImporter
return $out;
}
/**
* Builds a JSON-based constraint violation.
*
@ -964,25 +968,13 @@ class BOMImporter
* The violation includes a message, property path, invalid value, and other contextual information.
* Translations for the violation message can be applied through the translator service.
*
* @param string $message The translation key for the validation message.
* @param string $propertyPath The property path where the violation occurred.
* @param mixed|null $invalidValue The value that caused the violation (optional).
* @param array $parameters Additional parameters for message placeholders (default is an empty array).
* @param string $message The translation key for the validation message.
* @param string $propertyPath The property path where the violation occurred.
* @param mixed|null $invalidValue The value that caused the violation (optional).
* @param array $parameters Additional parameters for message placeholders (default is an empty array).
*
* @return ConstraintViolation The created constraint violation object.
*/
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
*/
@ -1460,4 +1452,30 @@ class BOMImporter
return array_values($headers);
}
/**
* Builds a JSON-based constraint violation.
*
* This method creates a `ConstraintViolation` object that represents a validation error.
* The violation includes a message, property path, invalid value, and other contextual information.
* Translations for the violation message can be applied through the translator service.
*
* @param string $message The translation key for the validation message.
* @param string $propertyPath The property path where the violation occurred.
* @param mixed|null $invalidValue The value that caused the violation (optional).
* @param array $parameters Additional parameters for message placeholders (default is an empty array).
*
* @return ConstraintViolation The created constraint violation object.
*/
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
);
}
}

View file

@ -36,6 +36,7 @@ use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Helpers\Assemblies\AssemblyPartAggregator;
use App\Helpers\FilenameSanatizer;
use App\Serializer\APIPlatform\SkippableItemNormalizer;
@ -379,49 +380,62 @@ class EntityExporter
],
Project::class => [
'header' => [
'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName', 'BomQuantity',
'BomPartId', 'BomPartIpn', 'BomPartMpnr', 'BomPartName', 'BomDesignator', 'BomPartDescription',
'BomMountNames'
'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName',
//BOM relevant attributes
'Quantity', 'PartId', 'PartName', 'Ipn', 'Manufacturer', 'Mpn', 'Name', 'Designator',
'Description', 'MountNames'
],
'processEntity' => fn($entity, $depth) => [
'ProjectId' => $entity->getId(),
'ParentProjectId' => $entity->getParent()?->getId() ?? '',
'Id' => $entity->getId(),
'ParentId' => $entity->getParent()?->getId() ?? '',
'Type' => 'project',
'ProjectNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(),
'ProjectName' => $entity->getName(),
'ProjectFullName' => $this->getFullName($entity),
'BomQuantity' => '-',
'BomPartId' => '-',
'BomPartIpn' => '-',
'BomPartMpnr' => '-',
'BomPartName' => '-',
'BomDesignator' => '-',
'BomPartDescription' => '-',
'BomMountNames' => '-',
//BOM relevant attributes
'Quantity' => '-',
'PartId' => '-',
'PartName' => '-',
'Ipn' => '-',
'Manufacturer' => '-',
'Mpn' => '-',
'Name' => '-',
'Designator' => '-',
'Description' => '-',
'MountNames' => '-',
],
'processBomEntries' => fn($entity, $depth) => array_map(fn(AssemblyBOMEntry $bomEntry) => [
'processBomEntries' => fn($entity, $depth) => array_map(fn(ProjectBOMEntry $bomEntry) => [
'Id' => $entity->getId(),
'ParentId' => '',
'Type' => 'project_bom_entry',
'ProjectNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(),
'ProjectName' => $entity->getName(),
'ProjectFullName' => $this->getFullName($entity),
'BomQuantity' => $bomEntry->getQuantity() ?? '',
'BomPartId' => $bomEntry->getPart()?->getId() ?? '',
'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '',
'BomPartMpnr' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '',
'BomPartName' => $bomEntry->getPart()?->getName() ?? '',
'BomDesignator' => $bomEntry->getName() ?? '',
'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '',
'BomMountNames' => $bomEntry->getMountNames(),
//BOM relevant attributes
'Quantity' => $bomEntry->getQuantity() ?? '',
'PartId' => $bomEntry->getPart()?->getId() ?? '',
'PartName' => $bomEntry->getPart()?->getName() ?? '',
'Ipn' => $bomEntry->getPart()?->getIpn() ?? '',
'Manufacturer' => $bomEntry->getPart()?->getManufacturer()?->getName() ?? '',
'Mpn' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '',
'Name' => $bomEntry->getPart()?->getName() ?? '',
'Designator' => $bomEntry->getMountnames() ?? '',
'Description' => $bomEntry->getPart()?->getDescription() ?? '',
'MountNames' => $bomEntry->getMountNames(),
], $entity->getBomEntries()->toArray()),
],
Assembly::class => [
'header' => [
'Id', 'ParentId', 'Type', 'AssemblyIpn', 'AssemblyNameHierarchical', 'AssemblyName',
'AssemblyFullName', 'BomQuantity', 'BomMultiplier', 'BomPartId', 'BomPartIpn', 'BomPartMpnr',
'BomPartName', 'BomDesignator', 'BomPartDescription', 'BomMountNames', 'BomReferencedAssemblyId',
'BomReferencedAssemblyIpn', 'BomReferencedAssemblyFullName'
'AssemblyFullName',
//BOM relevant attributes
'Quantity', 'PartId', 'PartName', 'Ipn', 'Manufacturer', 'Mpn', 'Name', 'Designator',
'Description', 'MountNames', 'ReferencedAssemblyId', 'ReferencedAssemblyIpn',
'ReferencedAssemblyFullName'
],
'processEntity' => fn($entity, $depth) => [
'Id' => $entity->getId(),
@ -431,18 +445,21 @@ class EntityExporter
'AssemblyNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(),
'AssemblyName' => $entity->getName(),
'AssemblyFullName' => $this->getFullName($entity),
'BomQuantity' => '-',
'BomMultiplier' => '-',
'BomPartId' => '-',
'BomPartIpn' => '-',
'BomPartMpnr' => '-',
'BomPartName' => '-',
'BomDesignator' => '-',
'BomPartDescription' => '-',
'BomMountNames' => '-',
'BomReferencedAssemblyId' => '-',
'BomReferencedAssemblyIpn' => '-',
'BomReferencedAssemblyFullName' => '-',
//BOM relevant attributes
'Quantity' => '-',
'PartId' => '-',
'PartName' => '-',
'Ipn' => '-',
'Manufacturer' => '-',
'Mpn' => '-',
'Name' => '-',
'Designator' => '-',
'Description' => '-',
'MountNames' => '-',
'ReferencedAssemblyId' => '-',
'ReferencedAssemblyIpn' => '-',
'ReferencedAssemblyFullName' => '-',
],
'processBomEntries' => fn($entity, $depth) => $this->processBomEntriesWithAggregatedParts($entity, $depth),
],
@ -555,6 +572,7 @@ class EntityExporter
{
$rows = [];
/** @var AssemblyBOMEntry $bomEntry */
foreach ($assembly->getBomEntries() as $bomEntry) {
// Add the BOM entry itself
$rows[] = [
@ -565,18 +583,21 @@ class EntityExporter
'AssemblyNameHierarchical' => str_repeat('--', $depth) . '> ' . $assembly->getName(),
'AssemblyName' => $assembly->getName(),
'AssemblyFullName' => $this->getFullName($assembly),
'BomQuantity' => $bomEntry->getQuantity() ?? '',
'BomMultiplier' => '',
'BomPartId' => $bomEntry->getPart()?->getId() ?? '-',
'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '-',
'BomPartMpnr' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-',
'BomPartName' => $bomEntry->getPart()?->getName() ?? '-',
'BomDesignator' => $bomEntry->getName() ?? '-',
'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '-',
'BomMountNames' => $bomEntry->getMountNames(),
'BomReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '-',
'BomReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '-',
'BomReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null),
//BOM relevant attributes
'Quantity' => $bomEntry->getQuantity() ?? '',
'PartId' => $bomEntry->getPart()?->getId() ?? '-',
'PartName' => $bomEntry->getPart()?->getName() ?? '-',
'Ipn' => $bomEntry->getPart()?->getIpn() ?? '-',
'Manufacturer' => $bomEntry->getPart()?->getManufacturer()?->getName() ?? '-',
'Mpn' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-',
'Name' => $bomEntry->getName() ?? '-',
'Designator' => $bomEntry->getDesignator(),
'MountNames' => $bomEntry->getMountNames(),
'Description' => $bomEntry->getPart()?->getDescription() ?? '-',
'ReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '-',
'ReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '-',
'ReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null),
];
// If a referenced assembly exists, add aggregated parts
@ -597,18 +618,21 @@ class EntityExporter
'AssemblyNameHierarchical' => '',
'AssemblyName' => $partAssembly ? $partAssembly->getName() : '',
'AssemblyFullName' => $this->getFullName($partAssembly),
'BomQuantity' => $partData['quantity'],
'BomMultiplier' => $partData['multiplier'],
'BomPartId' => $partData['part']?->getId(),
'BomPartIpn' => $partData['part']?->getIpn(),
'BomPartMpnr' => $partData['part']?->getManufacturerProductNumber(),
'BomPartName' => $partData['part']?->getName(),
'BomDesignator' => $partData['part']?->getName(),
'BomPartDescription' => $partData['part']?->getDescription(),
'BomMountNames' => '-',
'BomReferencedAssemblyId' => '-',
'BomReferencedAssemblyIpn' => '-',
'BomReferencedAssemblyFullName' => '-',
//BOM relevant attributes
'Quantity' => $partData['quantity'],
'PartId' => $partData['part']?->getId(),
'PartName' => $partData['part']?->getName(),
'Ipn' => $partData['part']?->getIpn(),
'Manufacturer' => $partData['part']?->getManufacturer()?->getName(),
'Mpn' => $partData['part']?->getManufacturerProductNumber(),
'Name' => $partData['name'] ?? '',
'Designator' => $partData['designator'],
'Description' => $partData['part']?->getDescription(),
'MountNames' => '-',
'ReferencedAssemblyId' => '-',
'ReferencedAssemblyIpn' => '-',
'ReferencedAssemblyFullName' => '-',
];
}
}