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

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\DataTables\LogDataTable;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Attachments\AttachmentUpload;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;

View file

@ -194,14 +194,14 @@ class ProjectController extends AbstractController
// For PCB imports, proceed directly
$importerResult = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [
'type' => $form->get('type')->getData(),
'type' => $import_type,
]);
// Validate the project entries
$errors = $validator->validateProperty($project, 'bom_entries');
//If no validation errors occured, save the changes and redirect to edit page
if (count ($errors) === 0 && $importerResult->getViolations()->count() === 0) {
//If no validation errors occurred, save the changes and redirect to edit page
if (count($errors) === 0 && $importerResult->getViolations()->count() === 0) {
$entries = $importerResult->getBomEntries();
$this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)]));

View file

@ -162,6 +162,12 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface
return $html;
},
])
->add('designator', TextColumn::class, [
'label' => 'assembly.bom.designator',
'render' => function ($value, AssemblyBOMEntry $context) {
return htmlspecialchars($context->getDesignator());
},
])
->add('instockAmount', TextColumn::class, [
'label' => 'assembly.bom.instockAmount',
'visible' => false,
@ -195,7 +201,7 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface
$this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->tableSettings->assembliesBomDefaultColumns,
"TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS");
$dataTable->addOrderBy('name', DataTable::SORT_ASCENDING);
$dataTable->addOrderBy('name');
$dataTable->createAdapter(ORMAdapter::class, [
'entity' => Attachment::class,

View file

@ -282,34 +282,6 @@ final class PartsDataTable implements DataTableTypeInterface
]);
}
//Add a assembly column to list where the part is used, when the user has the permission to see the assemblies
if ($this->security->isGranted('read', Assembly::class)) {
$this->csh->add('assemblies', TextColumn::class, [
'label' => $this->translator->trans('assembly.labelp'),
'render' => function ($value, Part $context): string {
//Only show the first 5 assembly names
$assemblies = $context->getAssemblies();
$tmp = "";
$max = 5;
for ($i = 0; $i < min($max, count($assemblies)); $i++) {
$url = $this->urlGenerator->infoURL($assemblies[$i]);
$tmp .= sprintf('<a href="%s">%s</a>', $url, htmlspecialchars($assemblies[$i]->getName()));
if ($i < count($assemblies) - 1) {
$tmp .= ", ";
}
}
if (count($assemblies) > $max) {
$tmp .= ", + ".(count($assemblies) - $max);
}
return $tmp;
}
]);
}
$this->csh
->add('edit', IconLinkColumn::class, [
'label' => $this->translator->trans('part.table.edit'),

View file

@ -36,8 +36,6 @@ use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Contracts\TimeStampableInterface;
use App\Entity\ProjectSystem\Project;
use App\Entity\AssemblySystem\Assembly;
use App\Repository\DBElementRepository;
use App\Validator\Constraints\AssemblySystem\AssemblyCycle;
use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry;
@ -86,7 +84,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
#[ApiFilter(LikeFilter::class, properties: ["name", 'mountnames', 'designator', "comment"])]
#[ApiFilter(RangeFilter::class, properties: ['quantity'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified', 'quantity'])]
class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInterface, TimeStampableInterface
@ -105,6 +103,13 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
protected string $mountnames = '';
/**
* @var string Reference mark on the circuit diagram/PCB
*/
#[ORM\Column(name: 'designator', type: Types::TEXT)]
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
protected string $designator = '';
/**
* @var string|null An optional name describing this BOM entry (useful for non-part entries)
*/
@ -192,6 +197,16 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt
return $this;
}
public function getDesignator(): string
{
return $this->designator;
}
public function setDesignator(string $designator): void
{
$this->designator = $designator;
}
/**
* @return string
*/

View file

@ -61,9 +61,9 @@ use App\Entity\Attachments\UserAttachment;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\LogWithEventUndoInterface;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\Parameters\AssemblyParameter;
use App\Entity\Parameters\PartCustomStateParameter;
use App\Entity\Parts\PartCustomState;
use App\Entity\Parameters\AssemblyParameter;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parameters\AttachmentTypeParameter;

View file

@ -22,6 +22,8 @@ declare(strict_types=1);
*/
namespace App\Entity\LogSystem;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType;
use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
@ -78,6 +80,9 @@ enum LogTargetType: int
case ASSEMBLY_BOM_ENTRY = 24;
case PART_CUSTOM_STATE = 25;
case ASSEMBLY = 23;
case ASSEMBLY_BOM_ENTRY = 24;
/**
* Returns the class name of the target type or null if the target type is NONE.
* @return string|null

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Entity\Parts\PartTraits\AssemblyTrait;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;

View file

@ -45,12 +45,17 @@ class AssemblyBOMEntryType extends AbstractType
])
->add('name', TextType::class, [
'label' => 'assembly.bom.name',
'help' => 'assembly.bom.name.help',
'required' => false,
])
->add('designator', TextType::class, [
'label' => 'assembly.bom.designator',
'help' => 'assembly.bom.designator.help',
'required' => false
])
->add('mountnames', TextType::class, [
'required' => false,
'label' => 'assembly.bom.mountnames',
'empty_data' => '',
'attr' => [
'class' => 'tagsinput',
'data-controller' => 'elements--tagsinput',

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Helpers\Assemblies;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use App\Entity\Parts\Part;
use Dompdf\Dompdf;
use Dompdf\Options;
@ -61,6 +62,7 @@ class AssemblyPartAggregator
*/
private function processAssembly(Assembly $assembly, float $multiplier, array &$aggregatedParts): void
{
/** @var AssemblyBOMEntry $bomEntry */
foreach ($assembly->getBomEntries() as $bomEntry) {
// If the BOM entry refers to a part, add its quantity
if ($bomEntry->getPart() instanceof Part) {
@ -70,6 +72,8 @@ class AssemblyPartAggregator
$aggregatedParts[$part->getId()] = [
'part' => $part,
'assembly' => $assembly,
'name' => $bomEntry->getName(),
'designator' => $bomEntry->getDesignator(),
'quantity' => $bomEntry->getQuantity(),
'multiplier' => $multiplier,
];
@ -81,6 +85,8 @@ class AssemblyPartAggregator
$aggregatedParts[] = [
'part' => null,
'assembly' => $assembly,
'name' => $bomEntry->getName(),
'designator' => $bomEntry->getDesignator(),
'quantity' => $bomEntry->getQuantity(),
'multiplier' => $multiplier,
];

View file

@ -103,7 +103,6 @@ class AssemblyBuildHelper
}
/**
* Returns the assembly BOM entries for which parts are missing in the stock for the given number of builds
* Returns the referenced assembly BOM entries for which parts are missing in the stock for the given number of builds
* @param Assembly $assembly The assembly for which the BOM entries should be checked
* @param int $number_of_builds How often should the assembly be build?

View file

@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Services;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Attachments\AttachmentType;

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' => '-',
];
}
}

View file

@ -51,15 +51,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class ToolsTreeBuilder
{
public function __construct(
protected TranslatorInterface $translator,
protected UrlGeneratorInterface $urlGenerator,
protected TagAwareCacheInterface $cache,
protected UserCacheKeyGenerator $keyGenerator,
protected Security $security,
protected ?array $dataSourceSynonyms = [],
) {
$this->dataSourceSynonyms = $dataSourceSynonyms ?? [];
public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security)
{
}
/**
@ -174,43 +167,43 @@ class ToolsTreeBuilder
}
if ($this->security->isGranted('read', new Category())) {
$nodes[] = (new TreeViewNode(
$this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()),
$this->translator->trans('tree.tools.edit.categories'),
$this->urlGenerator->generate('category_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-tags');
}
if ($this->security->isGranted('read', new Project())) {
$nodes[] = (new TreeViewNode(
$this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()),
$this->translator->trans('tree.tools.edit.projects'),
$this->urlGenerator->generate('project_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-archive');
}
if ($this->security->isGranted('read', new Assembly())) {
$nodes[] = (new TreeViewNode(
$this->getTranslatedDataSourceOrSynonym('assembly', 'tree.tools.edit.assemblies', $this->translator->getLocale()),
$this->translator->trans('tree.tools.edit.assemblies'),
$this->urlGenerator->generate('assembly_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-list');
}
if ($this->security->isGranted('read', new Supplier())) {
$nodes[] = (new TreeViewNode(
$this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()),
$this->translator->trans('tree.tools.edit.suppliers'),
$this->urlGenerator->generate('supplier_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-truck');
}
if ($this->security->isGranted('read', new Manufacturer())) {
$nodes[] = (new TreeViewNode(
$this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()),
$this->translator->trans('tree.tools.edit.manufacturer'),
$this->urlGenerator->generate('manufacturer_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-industry');
}
if ($this->security->isGranted('read', new StorageLocation())) {
$nodes[] = (new TreeViewNode(
$this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()),
$this->translator->trans('tree.tools.edit.storelocation'),
$this->urlGenerator->generate('store_location_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-cube');
}
if ($this->security->isGranted('read', new Footprint())) {
$nodes[] = (new TreeViewNode(
$this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()),
$this->translator->trans('tree.tools.edit.footprint'),
$this->urlGenerator->generate('footprint_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-microchip');
}
@ -324,22 +317,4 @@ class ToolsTreeBuilder
return $nodes;
}
protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string
{
$currentTranslation = $this->translator->trans($translationKey);
// Call alternatives from DataSourcesynonyms (if available)
if (!empty($this->dataSourceSynonyms[$dataSource][$locale])) {
$alternativeTranslation = $this->dataSourceSynonyms[$dataSource][$locale];
// Use alternative translation when it deviates from the standard translation
if ($alternativeTranslation !== $currentTranslation) {
return $alternativeTranslation;
}
}
// Otherwise return the standard translation
return $currentTranslation;
}
}

View file

@ -68,11 +68,9 @@ class TreeViewGenerator
protected TranslatorInterface $translator,
private readonly UrlGeneratorInterface $router,
private readonly SidebarSettings $sidebarSettings,
protected ?array $dataSourceSynonyms = [],
) {
$this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled;
$this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded;
$this->dataSourceSynonyms = $dataSourceSynonyms ?? [];
}
/**
@ -228,16 +226,14 @@ class TreeViewGenerator
protected function entityClassToRootNodeString(string $class): string
{
$locale = $this->translator->getLocale();
return match ($class) {
Category::class => $this->getTranslatedOrSynonym('category', $locale),
StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale),
Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale),
Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale),
Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale),
Project::class => $this->getTranslatedOrSynonym('project', $locale),
Assembly::class => $this->getTranslatedOrSynonym('assembly', $locale),
Category::class => $this->translator->trans('category.labelp'),
StorageLocation::class => $this->translator->trans('storelocation.labelp'),
Footprint::class => $this->translator->trans('footprint.labelp'),
Manufacturer::class => $this->translator->trans('manufacturer.labelp'),
Supplier::class => $this->translator->trans('supplier.labelp'),
Project::class => $this->translator->trans('project.labelp'),
Assembly::class => $this->translator->trans('assembly.labelp'),
default => $this->translator->trans('tree.root_node.text'),
};
}
@ -294,22 +290,4 @@ class TreeViewGenerator
return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line
});
}
protected function getTranslatedOrSynonym(string $key, string $locale): string
{
$currentTranslation = $this->translator->trans($key . '.labelp');
// Call alternatives from DataSourcesynonyms (if available)
if (!empty($this->dataSourceSynonyms[$key][$locale])) {
$alternativeTranslation = $this->dataSourceSynonyms[$key][$locale];
// Use alternative translation when it deviates from the standard translation
if ($alternativeTranslation !== $currentTranslation) {
return $alternativeTranslation;
}
}
// Otherwise return the standard translation
return $currentTranslation;
}
}

View file

@ -28,12 +28,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
enum AssemblyBomTableColumns : string implements TranslatableInterface
{
case NAME = "name";
case ID = "id";
case QUANTITY = "quantity";
case IPN = "ipn";
case DESCRIPTION = "description";
case CATEGORY = "category";
case MANUFACTURER = "manufacturer";
case DESIGNATOR = "designator";
case MOUNTNAMES = "mountnames";
case STORAGE_LOCATION = "storage_location";
case AMOUNT = "amount";
case ADDED_DATE = "addedDate";
case LAST_MODIFIED = "lastModified";
case EDIT = "edit";
public function trans(TranslatorInterface $translator, ?string $locale = null): string
{

View file

@ -97,8 +97,8 @@ class TableSettings
#[Assert\Unique()]
#[Assert\All([new Assert\Type(AssemblyBomTableColumns::class)])]
public array $assembliesBomDefaultColumns = [AssemblyBomTableColumns::QUANTITY, AssemblyTableColumns::ID, AssemblyTableColumns::IPN,
AssemblyTableColumns::NAME, AssemblyTableColumns::DESCRIPTION];
public array $assembliesBomDefaultColumns = [AssemblyBomTableColumns::QUANTITY, AssemblyBomTableColumns::ID,
AssemblyBomTableColumns::IPN, AssemblyBomTableColumns::NAME, AssemblyBomTableColumns::DESCRIPTION];
#[SettingsParameter(label: new TM("settings.behavior.table.preview_image_min_width"),
formOptions: ['attr' => ['min' => 1, 'max' => 100]],