From ebba9cfc514c7ba4306ae14e093051d406296775 Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Sun, 29 Mar 2026 18:07:39 +0200 Subject: [PATCH 01/11] Fix identation --- src/DataTables/ProjectBomEntriesDataTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 0d05c2484..c0c936591 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -149,7 +149,7 @@ public function configure(DataTable $dataTable, array $options): void ->add('manufacturing_status', EnumColumn::class, [ 'label' => $this->translator->trans('part.table.manufacturingStatus'), - 'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(), + 'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(), 'class' => ManufacturingStatus::class, 'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string { if ($status === null) { From d9773becfd40e817b270c1071292bbb881fc3495 Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Sun, 29 Mar 2026 18:14:06 +0200 Subject: [PATCH 02/11] Allow ordering of column Storage Locations in BOM fix-#1152 --- src/DataTables/ProjectBomEntriesDataTable.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index c0c936591..ba83bcc23 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -185,6 +185,7 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('storageLocations', TextColumn::class, [ 'label' => 'part.table.storeLocations', + 'orderField' => 'NATSORT(MIN(storageLocations.name))', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { if ($context->getPart() !== null) { From 2c607c7e269b82527c5a5a62ae56d163034366e3 Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Sun, 29 Mar 2026 19:28:23 +0200 Subject: [PATCH 03/11] Fix "[Semantical Error] line 0, col 274 near 'storageLocations.name))': Error: 'storageLocations' is not defined." when trying to sort by column Storage Locations --- src/DataTables/ProjectBomEntriesDataTable.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index ba83bcc23..4bf108550 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -231,6 +231,8 @@ private function getQuery(QueryBuilder $builder, array $options): void ->leftJoin('part.category', 'category') ->leftJoin('part.footprint', 'footprint') ->leftJoin('part.manufacturer', 'manufacturer') + ->leftJoin('part.partLots', 'partLots') + ->leftJoin('partLots.storage_location', 'storageLocations') ->where('bom_entry.project = :project') ->setParameter('project', $options['project']) ; From 5c5c7cece179df07d3c5b16562d705e9ff9608fb Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Sun, 29 Mar 2026 22:04:10 +0200 Subject: [PATCH 04/11] Try to fix "Iterate with fetch join in class App\Entity\Parts\PartLot using association part not allowed." when opening BOM --- src/DataTables/ProjectBomEntriesDataTable.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 4bf108550..a3e821812 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -185,7 +185,7 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('storageLocations', TextColumn::class, [ 'label' => 'part.table.storeLocations', - 'orderField' => 'NATSORT(MIN(storageLocations.name))', + 'orderField' => 'NATSORT(storageLocationOrder)', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { if ($context->getPart() !== null) { @@ -226,6 +226,7 @@ private function getQuery(QueryBuilder $builder, array $options): void { $builder->select('bom_entry') ->addSelect('part') + ->addSelect('(SELECT MIN(storageLocationSort.name) FROM ' . \App\Entity\Parts\PartLot::class . ' partLotSort LEFT JOIN partLotSort.storage_location storageLocationSort WHERE partLotSort.part = part) AS HIDDEN storageLocationOrder') ->from(ProjectBOMEntry::class, 'bom_entry') ->leftJoin('bom_entry.part', 'part') ->leftJoin('part.category', 'category') From 75890e199a3eeafe94161e89e8d00249376bf5fa Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Sun, 29 Mar 2026 22:29:09 +0200 Subject: [PATCH 05/11] Revert "Try to fix "Iterate with fetch join in class App\Entity\Parts\PartLot using association part not allowed." when opening BOM" This reverts commit 5c5c7cece179df07d3c5b16562d705e9ff9608fb. --- src/DataTables/ProjectBomEntriesDataTable.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index a3e821812..4bf108550 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -185,7 +185,7 @@ public function configure(DataTable $dataTable, array $options): void ]) ->add('storageLocations', TextColumn::class, [ 'label' => 'part.table.storeLocations', - 'orderField' => 'NATSORT(storageLocationOrder)', + 'orderField' => 'NATSORT(MIN(storageLocations.name))', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { if ($context->getPart() !== null) { @@ -226,7 +226,6 @@ private function getQuery(QueryBuilder $builder, array $options): void { $builder->select('bom_entry') ->addSelect('part') - ->addSelect('(SELECT MIN(storageLocationSort.name) FROM ' . \App\Entity\Parts\PartLot::class . ' partLotSort LEFT JOIN partLotSort.storage_location storageLocationSort WHERE partLotSort.part = part) AS HIDDEN storageLocationOrder') ->from(ProjectBOMEntry::class, 'bom_entry') ->leftJoin('bom_entry.part', 'part') ->leftJoin('part.category', 'category') From 9516062e8242532289ea98199cd237157dc57c35 Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Sun, 29 Mar 2026 23:33:46 +0200 Subject: [PATCH 06/11] Try to fix "Iterate with fetch join in class App\Entity\Parts\PartLot using association part not allowed." when opening BOM 2nd try --- src/DataTables/ProjectBomEntriesDataTable.php | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 4bf108550..5d7510983 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -183,9 +183,10 @@ public function configure(DataTable $dataTable, array $options): void return ''; } ]) - ->add('storageLocations', TextColumn::class, [ - 'label' => 'part.table.storeLocations', - 'orderField' => 'NATSORT(MIN(storageLocations.name))', + ->add('storelocation', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.storeLocations'), + //We need to use a aggregate function to get the first store location, as we have a one-to-many relation + 'orderField' => 'NATSORT(MIN(_storelocations.name))', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { if ($context->getPart() !== null) { @@ -194,7 +195,7 @@ public function configure(DataTable $dataTable, array $options): void return ''; } - ]) + ], alias: 'storage_location') ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), @@ -229,12 +230,22 @@ private function getQuery(QueryBuilder $builder, array $options): void ->from(ProjectBOMEntry::class, 'bom_entry') ->leftJoin('bom_entry.part', 'part') ->leftJoin('part.category', 'category') + ->leftJoin('part.partLots', 'partLots') + ->leftJoin('partLots.storage_location', 'storelocations') ->leftJoin('part.footprint', 'footprint') ->leftJoin('part.manufacturer', 'manufacturer') - ->leftJoin('part.partLots', 'partLots') - ->leftJoin('partLots.storage_location', 'storageLocations') + ->leftJoin('part.partCustomState', 'partCustomState') ->where('bom_entry.project = :project') ->setParameter('project', $options['project']) + + //We have to group by all elements, or only the first sub elements of an association is fetched! (see issue #190) + ->addGroupBy('part') + ->addGroupBy('partLots') + ->addGroupBy('category') + ->addGroupBy('storelocations') + ->addGroupBy('footprint') + ->addGroupBy('manufacturer') + ->addGroupBy('partCustomState') ; } From d13353c09fc46425bd31453d7443e08866317a73 Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Sun, 29 Mar 2026 23:37:19 +0200 Subject: [PATCH 07/11] Remove alias to fix: Unknown named parameter $alias --- src/DataTables/ProjectBomEntriesDataTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 5d7510983..c4870cf81 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -195,7 +195,7 @@ public function configure(DataTable $dataTable, array $options): void return ''; } - ], alias: 'storage_location') + ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), From c42c98983151f1223f75dcd732cc53ab7877cd68 Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Fri, 3 Apr 2026 22:40:22 +0200 Subject: [PATCH 08/11] Reformat code to allow easier diff between ProjectBomEntriesDataTable.php and PartsDataTable.php --- src/DataTables/ProjectBomEntriesDataTable.php | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index c4870cf81..5ffe36467 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -1,8 +1,5 @@ . */ + +declare(strict_types=1); + namespace App\DataTables; use App\DataTables\Column\EntityColumn; @@ -44,9 +44,12 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface { - public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, - protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) - { + public function __construct( + protected EntityURLGenerator $entityURLGenerator, + protected TranslatorInterface $translator, + protected AmountFormatter $amountFormatter, + protected PartDataTableHelper $partDataTableHelper + ) { } @@ -62,7 +65,7 @@ public function configure(DataTable $dataTable, array $options): void return ''; } return $this->partDataTableHelper->renderPicture($context->getPart()); - }, + } ]) ->add('id', TextColumn::class, [ @@ -133,18 +136,18 @@ public function configure(DataTable $dataTable, array $options): void ->add('category', EntityColumn::class, [ 'label' => $this->translator->trans('part.table.category'), 'property' => 'part.category', - 'orderField' => 'NATSORT(category.name)', + 'orderField' => 'NATSORT(category.name)' ]) ->add('footprint', EntityColumn::class, [ 'property' => 'part.footprint', 'label' => $this->translator->trans('part.table.footprint'), - 'orderField' => 'NATSORT(footprint.name)', + 'orderField' => 'NATSORT(footprint.name)' ]) ->add('manufacturer', EntityColumn::class, [ 'property' => 'part.manufacturer', 'label' => $this->translator->trans('part.table.manufacturer'), - 'orderField' => 'NATSORT(manufacturer.name)', + 'orderField' => 'NATSORT(manufacturer.name)' ]) ->add('manufacturing_status', EnumColumn::class, [ @@ -225,7 +228,8 @@ function (QueryBuilder $builder) use ($options): void { private function getQuery(QueryBuilder $builder, array $options): void { - $builder->select('bom_entry') + $builder + ->select('bom_entry') ->addSelect('part') ->from(ProjectBOMEntry::class, 'bom_entry') ->leftJoin('bom_entry.part', 'part') @@ -238,7 +242,6 @@ private function getQuery(QueryBuilder $builder, array $options): void ->where('bom_entry.project = :project') ->setParameter('project', $options['project']) - //We have to group by all elements, or only the first sub elements of an association is fetched! (see issue #190) ->addGroupBy('part') ->addGroupBy('partLots') ->addGroupBy('category') From 1bb943d5ce9f7ec3a415c00d6d7943752fa816d9 Mon Sep 17 00:00:00 2001 From: Marc Kreidler Date: Fri, 3 Apr 2026 22:42:14 +0200 Subject: [PATCH 09/11] Try if 'data' es really needed as it is not used in PartDataTable.php --- src/DataTables/ProjectBomEntriesDataTable.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 5ffe36467..709987e7a 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -152,7 +152,6 @@ public function configure(DataTable $dataTable, array $options): void ->add('manufacturing_status', EnumColumn::class, [ 'label' => $this->translator->trans('part.table.manufacturingStatus'), - 'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(), 'class' => ManufacturingStatus::class, 'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string { if ($status === null) { From a8b5a138b107c11317da75e72627821393eae502 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 4 Apr 2026 18:33:13 +0200 Subject: [PATCH 10/11] Use TwoStepORMAdapter to enable sorting based on other columns like storage location, manufacturing status --- src/DataTables/ProjectBomEntriesDataTable.php | 63 +++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 709987e7a..5449df715 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -22,21 +22,22 @@ namespace App\DataTables; +use App\DataTables\Adapters\TwoStepORMAdapter; use App\DataTables\Column\EntityColumn; use App\DataTables\Column\EnumColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; use App\DataTables\Helpers\PartDataTableHelper; -use App\Entity\Attachments\Attachment; +use App\Doctrine\Helpers\FieldHelper; use App\Entity\Parts\Part; use App\Entity\Parts\ManufacturingStatus; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; +use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; -use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; @@ -152,6 +153,8 @@ public function configure(DataTable $dataTable, array $options): void ->add('manufacturing_status', EnumColumn::class, [ 'label' => $this->translator->trans('part.table.manufacturingStatus'), + 'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(), + 'orderField' => 'part.manufacturing_status', 'class' => ManufacturingStatus::class, 'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string { if ($status === null) { @@ -211,11 +214,13 @@ public function configure(DataTable $dataTable, array $options): void $dataTable->addOrderBy('name', DataTable::SORT_ASCENDING); - $dataTable->createAdapter(ORMAdapter::class, [ - 'entity' => Attachment::class, - 'query' => function (QueryBuilder $builder) use ($options): void { - $this->getQuery($builder, $options); + $dataTable->createAdapter(TwoStepORMAdapter::class, [ + 'entity' => ProjectBOMEntry::class, + 'hydrate' => AbstractQuery::HYDRATE_OBJECT, + 'filter_query' => function (QueryBuilder $builder) use ($options): void { + $this->getFilterQuery($builder, $options); }, + 'detail_query' => $this->getDetailQuery(...), 'criteria' => [ function (QueryBuilder $builder) use ($options): void { $this->buildCriteria($builder, $options); @@ -225,22 +230,56 @@ function (QueryBuilder $builder) use ($options): void { ]); } - private function getQuery(QueryBuilder $builder, array $options): void + private function getFilterQuery(QueryBuilder $builder, array $options): void { $builder - ->select('bom_entry') - ->addSelect('part') + ->select('bom_entry.id') ->from(ProjectBOMEntry::class, 'bom_entry') ->leftJoin('bom_entry.part', 'part') ->leftJoin('part.category', 'category') - ->leftJoin('part.partLots', 'partLots') - ->leftJoin('partLots.storage_location', 'storelocations') + ->leftJoin('part.partLots', '_partLots') + ->leftJoin('_partLots.storage_location', '_storelocations') ->leftJoin('part.footprint', 'footprint') ->leftJoin('part.manufacturer', 'manufacturer') ->leftJoin('part.partCustomState', 'partCustomState') ->where('bom_entry.project = :project') ->setParameter('project', $options['project']) + ->addGroupBy('bom_entry') + ->addGroupBy('part') + ->addGroupBy('category') + ->addGroupBy('footprint') + ->addGroupBy('manufacturer') + ->addGroupBy('partCustomState') + ; + } + + private function getDetailQuery(QueryBuilder $builder, array $filter_results): void + { + $ids = array_map(static fn (array $row) => $row['id'], $filter_results); + if ($ids === []) { + $ids = [-1]; + } + $builder + ->select('bom_entry') + ->addSelect('part') + ->addSelect('category') + ->addSelect('partLots') + ->addSelect('storelocations') + ->addSelect('footprint') + ->addSelect('manufacturer') + ->addSelect('partCustomState') + ->from(ProjectBOMEntry::class, 'bom_entry') + ->leftJoin('bom_entry.part', 'part') + ->leftJoin('part.category', 'category') + ->leftJoin('part.partLots', 'partLots') + ->leftJoin('partLots.storage_location', 'storelocations') + ->leftJoin('part.footprint', 'footprint') + ->leftJoin('part.manufacturer', 'manufacturer') + ->leftJoin('part.partCustomState', 'partCustomState') + ->where('bom_entry.id IN (:ids)') + ->setParameter('ids', $ids) + ->addGroupBy('bom_entry') ->addGroupBy('part') ->addGroupBy('partLots') ->addGroupBy('category') @@ -249,6 +288,8 @@ private function getQuery(QueryBuilder $builder, array $options): void ->addGroupBy('manufacturer') ->addGroupBy('partCustomState') ; + + FieldHelper::addOrderByFieldParam($builder, 'bom_entry.id', 'ids'); } private function buildCriteria(QueryBuilder $builder, array $options): void From 92507a5c6d8cd1d7f5b0129c18f461da11e81175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 6 Apr 2026 15:09:35 +0200 Subject: [PATCH 11/11] Add readonly hint to projectBom query --- src/DataTables/ProjectBomEntriesDataTable.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 5449df715..04d8206bd 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -36,6 +36,7 @@ use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Column\TextColumn; @@ -287,6 +288,9 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->addGroupBy('footprint') ->addGroupBy('manufacturer') ->addGroupBy('partCustomState') + + ->setHint(Query::HINT_READ_ONLY, true) + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, false) ; FieldHelper::addOrderByFieldParam($builder, 'bom_entry.id', 'ids');