Baugruppen Stückliste um referenzierte Baugruppe erweitern

This commit is contained in:
Marcel Diegelmann 2025-07-03 13:38:51 +02:00
parent 14b0665daa
commit c7aa730bc3
22 changed files with 332 additions and 116 deletions

View file

@ -183,9 +183,12 @@ services:
App\DataTables\Helpers\ColumnSortHelper:
shared: false # Service has a state so not share it between different tables
App\DataTables\AssemblyDataTable:
arguments:
$visible_columns: '%partdb.table.assemblies.default_columns%'
App\DataTables\AssemblyBomEntriesDataTable:
arguments:
$visible_columns: '%partdb.table.assemblies.default_columns%'
arguments:
$visible_columns: '%partdb.table.assemblies_bom.default_columns%'
####################################################################################################################
# Label system

View file

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Form\Type;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Attachments\Attachment;
use App\Services\Attachments\AssemblyPreviewGenerator;
use App\Services\Attachments\AttachmentURLGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Event\PreSetDataEvent;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class AssemblySelectType extends AbstractType implements DataMapperInterface
{
public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em, private readonly AssemblyPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
//At initialization, we have to fill the form element with our selected data, so the user can see it
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
$form = $event->getForm();
$config = $form->getConfig()->getOptions();
$data = $event->getData() ?? [];
$config['compound'] = false;
$config['choices'] = is_iterable($data) ? $data : [$data];
$config['error_bubbling'] = true;
$form->add('autocomplete', EntityType::class, $config);
});
//After form submit, we have to add the selected element as choice, otherwise the form will not accept this element
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
$options = $form->get('autocomplete')->getConfig()->getOptions();
if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) {
$options['choices'] = [];
} else {
//Extract the ID from the submitted data
$id = $data['autocomplete'];
//Find the element in the database
$element = $this->em->find($options['class'], $id);
//Add the element as choice
$options['choices'] = [$element];
$options['error_bubbling'] = true;
$form->add('autocomplete', EntityType::class, $options);
}
});
$builder->setDataMapper($this);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'class' => Assembly::class,
'choice_label' => 'name',
'compound' => true,
'error_bubbling' => false,
]);
error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']));
$resolver->setDefaults([
'attr' => [
'data-controller' => 'elements--assembly-select',
'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']),
'autocomplete' => 'off',
],
]);
$resolver->setDefaults([
//Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request
'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) {
if($assembly instanceof Assembly) {
//Determine the picture to show:
$preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly);
if ($preview_attachment instanceof Attachment) {
$preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment,
'thumbnail_sm');
} else {
$preview_url = '';
}
}
return $assembly instanceof Assembly ? [
'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '',
'data-category' => '',
'data-footprint' => '',
'data-image' => $preview_url,
] : [];
})
]);
}
public function mapDataToForms($data, \Traversable $forms): void
{
$form = current(iterator_to_array($forms, false));
$form->setData($data);
}
public function mapFormsToData(\Traversable $forms, &$data): void
{
$form = current(iterator_to_array($forms, false));
$data = $form->getData();
}
}

View file

@ -0,0 +1,69 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Repository;
use App\Entity\AssemblySystem\Assembly;
/**
* @template TEntityClass of Assembly
* @extends DBElementRepository<TEntityClass>
*/
class AssemblyRepository extends StructuralDBElementRepository
{
/**
* @return Assembly[]
*/
public function autocompleteSearch(string $query, int $max_limits = 50): array
{
$qb = $this->createQueryBuilder('assembly');
$qb->select('assembly')
->where('ILIKE(assembly.name, :query) = TRUE')
->orWhere('ILIKE(assembly.description, :query) = TRUE');
$qb->setParameter('query', '%'.$query.'%');
$qb->setMaxResults($max_limits);
$qb->orderBy('NATSORT(assembly.name)', 'ASC');
return $qb->getQuery()->getResult();
}
}

View file

@ -0,0 +1,93 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Services\Attachments;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Attachments\Attachment;
class AssemblyPreviewGenerator
{
public function __construct(protected AttachmentManager $attachmentHelper)
{
}
/**
* Returns a list of attachments that can be used for previewing the assembly ordered by priority.
*
* @param Assembly $assembly the assembly for which the attachments should be determined
*
* @return (Attachment|null)[]
*
* @psalm-return list<Attachment|null>
*/
public function getPreviewAttachments(Assembly $assembly): array
{
$list = [];
//Master attachment has top priority
$attachment = $assembly->getMasterPictureAttachment();
if ($this->isAttachmentValidPicture($attachment)) {
$list[] = $attachment;
}
//Then comes the other images of the assembly
foreach ($assembly->getAttachments() as $attachment) {
//Dont show the master attachment twice
if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) {
$list[] = $attachment;
}
}
return $list;
}
/**
* Determines what attachment should be used for previewing a assembly (especially in assembly table).
* The returned attachment is guaranteed to be existing and be a picture.
*
* @param Assembly $assembly The assembly for which the attachment should be determined
*/
public function getTablePreviewAttachment(Assembly $assembly): ?Attachment
{
$attachment = $assembly->getMasterPictureAttachment();
if ($this->isAttachmentValidPicture($attachment)) {
return $attachment;
}
return null;
}
/**
* Checks if a attachment is exising and a valid picture.
*
* @param Attachment|null $attachment the attachment that should be checked
*
* @return bool true if the attachment is valid
*/
protected function isAttachmentValidPicture(?Attachment $attachment): bool
{
return $attachment instanceof Attachment
&& $attachment->isPicture()
&& $this->attachmentHelper->isFileExisting($attachment);
}
}

View file

@ -135,7 +135,7 @@
{% include "assemblies/info/_builds.html.twig" %}
</div>
<div class="tab-pane fade" id="attachments-tab-pane" role="tabpanel" aria-labelledby="attachments-tab" tabindex="0">
{% include "assemblies/info/_attachments_info.html.twig" with {"part": assembly} %}
{% include "assemblies/info/_attachments_info.html.twig" with {"assembly": assembly} %}
</div>
<div class="tab-pane fade" id="parameters-tab-pane" role="tabpanel" aria-labelledby="parameters-tab">
{% for name, parameters in assembly.groupedParameters %}

View file

@ -9996,12 +9996,6 @@ Element 3</target>
<target>Interní číslo dílu (IPN)</target>
</segment>
</unit>
<unit id="9uJEwv1" name="assembly.edit.ipn">
<segment state="translated">
<source>assembly.edit.ipn</source>
<target>Interní číslo dílu (IPN)</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
<segment state="translated">
<source>assembly.status.draft</source>
@ -13612,6 +13606,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
<target>Sestavy</target>
</segment>
</unit>
<unit id="Tab5Ou1" name="assembly.referencedAssembly.labelp">
<segment>
<source>assembly.referencedAssembly.labelp</source>
<target>Odkazované sestavy</target>
</segment>
</unit>
<unit id="gyRGdfv" name="assembly.edit">
<segment>
<source>assembly.edit</source>
@ -13780,6 +13780,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
<target>Sestavit</target>
</segment>
</unit>
<unit id="1bCA1zb" name="assembly.build.form.referencedAssembly">
<segment>
<source>assembly.build.form.referencedAssembly</source>
<target>Sestava "%name%"</target>
</segment>
</unit>
<unit id="LFSVVcP" name="assembly.builds.no_stocked_builds">
<segment>
<source>assembly.builds.no_stocked_builds</source>

View file

@ -10010,12 +10010,6 @@ Element 3</target>
<target>Internt Partnummer (IPN)</target>
</segment>
</unit>
<unit id="9uJEwv1" name="assembly.edit.ipn">
<segment state="translated">
<source>assembly.edit.ipn</source>
<target>Internt Partnummer (IPN)</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
<segment state="translated">
<source>assembly.status.draft</source>

View file

@ -2620,6 +2620,12 @@
<target>%value% (Μέρος)</target>
</segment>
</unit>
<unit id="o4zUJc5" name="part.table.name.value.for_assembly">
<segment state="translated">
<source>part.table.name.value.for_assembly</source>
<target>%value% (Συναρμολόγηση)</target>
</segment>
</unit>
<unit id="GW8ZOX7" name="part.table.name.value.for_project">
<segment state="translated">
<source>part.table.name.value.for_project</source>
@ -2629,7 +2635,7 @@
<unit id="hIIFtI1" name="assembly.edit.status">
<segment state="translated">
<source>assembly.edit.status</source>
<target>Κατάσταση</target>
<target>Κατάσταση συναρμολόγησης</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
@ -2692,6 +2698,12 @@
<target>Συναρμολογήσεις</target>
</segment>
</unit>
<unit id="Tab5Ou1" name="assembly.referencedAssembly.labelp">
<segment>
<source>assembly.referencedAssembly.labelp</source>
<target>Αναφερόμενες συναρμολογήσεις</target>
</segment>
</unit>
<unit id="gyRGdfv" name="assembly.edit">
<segment>
<source>assembly.edit</source>
@ -2860,6 +2872,12 @@
<target>Κατασκευή</target>
</segment>
</unit>
<unit id="1bCA1zb" name="assembly.build.form.referencedAssembly">
<segment>
<source>assembly.build.form.referencedAssembly</source>
<target>Συναρμολόγηση "%name%"</target>
</segment>
</unit>
<unit id="LFSVVcP" name="assembly.builds.no_stocked_builds">
<segment>
<source>assembly.builds.no_stocked_builds</source>
@ -2914,6 +2932,12 @@
<target>έργο</target>
</segment>
</unit>
<unit id="od1xTi3" name="assembly.bom.referencedAssembly">
<segment>
<source>assembly.bom.referencedAssembly</source>
<target>Συναρμολόγηση</target>
</segment>
</unit>
<unit id="PPsM0Dg" name="assembly.bom.name">
<segment>
<source>assembly.bom.name</source>
@ -2950,10 +2974,10 @@
<target>Εισαγωγή εξαρτημάτων συναρμολόγησης</target>
</segment>
</unit>
<unit id="WTasGao" name="assembly.bom.partOrProject">
<unit id="WTasGao" name="assembly.bom.partOrAssembly">
<segment>
<source>assembly.bom.partOrProject</source>
<target>Εξάρτημα</target>
<source>assembly.bom.partOrAssembly</source>
<target>Μέρος ή συναρμολόγηση</target>
</segment>
</unit>
<unit id="jHKh8Zp" name="assembly.bom.add_entry">

View file

@ -10089,12 +10089,6 @@ Element 1 -&gt; Element 1.2</target>
<target>Internal Part Number (IPN)</target>
</segment>
</unit>
<unit id="9uJEwv1" name="assembly.edit.ipn">
<segment state="translated">
<source>assembly.edit.ipn</source>
<target>Internal Part Number (IPN)</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
<segment state="translated">
<source>assembly.status.draft</source>

View file

@ -10020,12 +10020,6 @@ Elemento 3</target>
<target>Número de Componente Interno (IPN)</target>
</segment>
</unit>
<unit id="9uJEwv1" name="assembly.edit.ipn">
<segment state="translated">
<source>assembly.edit.ipn</source>
<target>Número de Componente Interno (IPN)</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
<segment state="translated">
<source>assembly.status.draft</source>

View file

@ -10022,12 +10022,6 @@ Element 3</target>
<target>Codice interno (IPN)</target>
</segment>
</unit>
<unit id="9uJEwv1" name="assembly.edit.ipn">
<segment state="translated">
<source>assembly.edit.ipn</source>
<target>Codice interno (IPN)</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
<segment state="translated">
<source>assembly.status.draft</source>

View file

@ -10025,12 +10025,6 @@ Element 3</target>
<target>Internal Part Number (IPN)</target>
</segment>
</unit>
<unit id="9uJEwv1" name="assembly.edit.ipn">
<segment state="translated">
<source>assembly.edit.ipn</source>
<target>Internal Part Number (IPN)</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
<segment state="translated">
<source>assembly.status.draft</source>

View file

@ -10029,12 +10029,6 @@
<target>Внутренний номер компонента (IPN)</target>
</segment>
</unit>
<unit id="9uJEwv1" name="assembly.edit.ipn">
<segment state="translated">
<source>assembly.edit.ipn</source>
<target>Внутренний номер компонента (IPN)</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
<segment state="translated">
<source>assembly.status.draft</source>
@ -13116,18 +13110,6 @@
<target>Проект</target>
</segment>
</unit>
<unit id="u7iO1dE" name="part.info.add_part_to_assembly">
<segment state="translated">
<source>part.info.add_part_to_assembly</source>
<target>Добавить эту часть в сборку</target>
</segment>
</unit>
<unit id="1zsGacg3" name="assembly.bom.project">
<segment>
<source>assembly.bom.project</source>
<target>Проект</target>
</segment>
</unit>
<unit id="od1xTi3" name="assembly.bom.referencedAssembly">
<segment>
<source>assembly.bom.referencedAssembly</source>

View file

@ -10028,12 +10028,6 @@ Element 3</target>
<target>内部零件号 (IPN)</target>
</segment>
</unit>
<unit id="9uJEwv1" name="assembly.edit.ipn">
<segment state="translated">
<source>assembly.edit.ipn</source>
<target>内部零件号 (IPN)</target>
</segment>
</unit>
<unit id="zRd.psv" name="assembly.status.draft">
<segment state="translated">
<source>assembly.status.draft</source>

View file

@ -251,12 +251,6 @@
<target>Du skal vælge en komponent eller angive et navn til en ikke-komponent styklistepost!</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.project.bom_entry.only_part_or_assembly_allowed">
<segment state="translated">
<source>validator.project.bom_entry.only_part_or_assembly_allowed</source>
<target>Det er kun tilladt at vælge én del eller en samling. Venligst tilpas dit valg!</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>

View file

@ -251,12 +251,6 @@
<target>Sie müssen ein Bauteil bzw. eine Baugruppe auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen!</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.project.bom_entry.only_part_or_assembly_allowed">
<segment state="translated">
<source>validator.project.bom_entry.only_part_or_assembly_allowed</source>
<target>Es darf nur ein Bauteil oder eine Baugruppe ausgewählt werden. Bitte passen Sie Ihre Auswahl an!</target>
</segment>
</unit>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>

View file

@ -251,12 +251,6 @@
<target>You have to select a part or assembly, or set a name for a non-component Bom entry!</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.project.bom_entry.only_part_or_assembly_allowed">
<segment state="translated">
<source>validator.project.bom_entry.only_part_or_assembly_allowed</source>
<target>Only one part or assembly may be selected. Please modify your selection!</target>
</segment>
</unit>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
@ -429,7 +423,6 @@
<segment>
<source>validator.assembly.bom_entry.name_or_part_needed</source>
<target>You must select a part or set a name for the entry!</target>
<target>You must select a part or set a name for the entry!</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.assembly.bom_entry.only_part_or_assembly_allowed">

View file

@ -251,12 +251,6 @@
<target>Morate odabrati dio za unos u BOM ili postaviti naziv za unos koji nije dio.</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.project.bom_entry.only_part_or_assembly_allowed">
<segment state="translated">
<source>validator.project.bom_entry.only_part_or_assembly_allowed</source>
<target>Dozvoljeno je odabrati samo jednu komponentu ili sklop. Molimo prilagodite svoj odabir!</target>
</segment>
</unit>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>

View file

@ -251,12 +251,6 @@
<target>È necessario selezionare un componente o assegnare un nome ad una voce BOM che non indica un componente!</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.project.bom_entry.only_part_or_assembly_allowed">
<segment state="translated">
<source>validator.project.bom_entry.only_part_or_assembly_allowed</source>
<target>È consentito selezionare solo una parte o un assieme. Si prega di modificare la selezione!</target>
</segment>
</unit>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>

View file

@ -251,12 +251,6 @@
<target>Należy wybrać część dla wpisu BOM części lub ustawić nazwę dla wpisu BOM niebędącego częścią.</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.project.bom_entry.only_part_or_assembly_allowed">
<segment state="translated">
<source>validator.project.bom_entry.only_part_or_assembly_allowed</source>
<target>Można wybrać tylko jedną część lub zespół. Proszę dostosować swój wybór!</target>
</segment>
</unit>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>

View file

@ -251,12 +251,6 @@
<target>Вам необходимо выбрать компонент или задать имя для BOM, не относящейся к компоненту!</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.project.bom_entry.only_part_or_assembly_allowed">
<segment state="translated">
<source>validator.project.bom_entry.only_part_or_assembly_allowed</source>
<target>Можно выбрать только деталь или сборку. Пожалуйста, измените ваш выбор!</target>
</segment>
</unit>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>

View file

@ -251,12 +251,6 @@
<target>您必须为 BOM 条目选择部件,或为非部件 BOM 条目设置名称。</target>
</segment>
</unit>
<unit id="Q42Zh.e" name="validator.project.bom_entry.only_part_or_assembly_allowed">
<segment state="translated">
<source>validator.project.bom_entry.only_part_or_assembly_allowed</source>
<target>只能选择一个零件或组件。请修改您的选择!</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>