Skip to content

Commit fe5442e

Browse files
committed
Simple batch processing
1 parent bbe4b01 commit fe5442e

File tree

9 files changed

+949
-2
lines changed

9 files changed

+949
-2
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace App\Controller;
24+
25+
use App\Entity\Parts\Part;
26+
use App\Entity\Parts\Supplier;
27+
use App\Form\InfoProviderSystem\GlobalFieldMappingType;
28+
use App\Services\InfoProviderSystem\PartInfoRetriever;
29+
use App\Services\InfoProviderSystem\ProviderRegistry;
30+
use App\Services\InfoProviderSystem\ExistingPartFinder;
31+
use Doctrine\ORM\EntityManagerInterface;
32+
use Psr\Log\LoggerInterface;
33+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
34+
use Symfony\Component\HttpClient\Exception\ClientException;
35+
use Symfony\Component\HttpFoundation\Request;
36+
use Symfony\Component\HttpFoundation\Response;
37+
use Symfony\Component\Routing\Attribute\Route;
38+
39+
use function Symfony\Component\Translation\t;
40+
41+
#[Route('/tools/bulk-info-provider-import')]
42+
class BulkInfoProviderImportController extends AbstractController
43+
{
44+
public function __construct(
45+
private readonly ProviderRegistry $providerRegistry,
46+
private readonly PartInfoRetriever $infoRetriever,
47+
private readonly ExistingPartFinder $existingPartFinder,
48+
private readonly EntityManagerInterface $entityManager
49+
) {
50+
}
51+
52+
#[Route('/step1', name: 'bulk_info_provider_step1')]
53+
public function step1(Request $request, LoggerInterface $exceptionLogger): Response
54+
{
55+
$this->denyAccessUnlessGranted('@info_providers.create_parts');
56+
57+
$ids = $request->query->get('ids');
58+
if (!$ids) {
59+
$this->addFlash('error', 'No parts selected for bulk import');
60+
return $this->redirectToRoute('homepage');
61+
}
62+
63+
// Get the selected parts
64+
$partIds = explode(',', $ids);
65+
$partRepository = $this->entityManager->getRepository(Part::class);
66+
$parts = $partRepository->getElementsFromIDArray($partIds);
67+
68+
if (empty($parts)) {
69+
$this->addFlash('error', 'No valid parts found for bulk import');
70+
return $this->redirectToRoute('homepage');
71+
}
72+
73+
// Generate field choices
74+
$fieldChoices = [
75+
'info_providers.bulk_search.field.mpn' => 'mpn',
76+
'info_providers.bulk_search.field.name' => 'name',
77+
];
78+
79+
// Add dynamic supplier fields
80+
$suppliers = $this->entityManager->getRepository(Supplier::class)->findAll();
81+
foreach ($suppliers as $supplier) {
82+
$supplierKey = strtolower(str_replace([' ', '-', '_'], '_', $supplier->getName()));
83+
$fieldChoices["Supplier: " . $supplier->getName() . " (SPN)"] = $supplierKey . '_spn';
84+
}
85+
86+
// Initialize form with useful default mappings
87+
$initialData = [
88+
'field_mappings' => [
89+
['field' => 'mpn', 'providers' => []]
90+
]
91+
];
92+
93+
$form = $this->createForm(GlobalFieldMappingType::class, $initialData, [
94+
'field_choices' => $fieldChoices
95+
]);
96+
$form->handleRequest($request);
97+
98+
$searchResults = null;
99+
100+
if ($form->isSubmitted() && $form->isValid()) {
101+
$fieldMappings = $form->getData()['field_mappings'];
102+
$searchResults = [];
103+
104+
foreach ($parts as $part) {
105+
$partResult = [
106+
'part' => $part,
107+
'search_results' => [],
108+
'errors' => []
109+
];
110+
111+
// Collect all DTOs from all applicable field mappings
112+
$allDtos = [];
113+
114+
foreach ($fieldMappings as $mapping) {
115+
$field = $mapping['field'];
116+
$providers = $mapping['providers'] ?? [];
117+
118+
if (empty($providers)) {
119+
continue;
120+
}
121+
122+
$keyword = $this->getKeywordFromField($part, $field);
123+
124+
if ($keyword) {
125+
try {
126+
$dtos = $this->infoRetriever->searchByKeyword(
127+
keyword: $keyword,
128+
providers: $providers
129+
);
130+
131+
// Add field info to each DTO for tracking
132+
foreach ($dtos as $dto) {
133+
$dto->_source_field = $field;
134+
$dto->_source_keyword = $keyword;
135+
}
136+
137+
$allDtos = array_merge($allDtos, $dtos);
138+
} catch (ClientException $e) {
139+
$partResult['errors'][] = "Error searching with {$field}: " . $e->getMessage();
140+
$exceptionLogger->error('Error during bulk info provider search for part ' . $part->getId() . " field {$field}: " . $e->getMessage(), ['exception' => $e]);
141+
}
142+
}
143+
}
144+
145+
// Remove duplicates based on provider_key + provider_id
146+
$uniqueDtos = [];
147+
$seenKeys = [];
148+
foreach ($allDtos as $dto) {
149+
$key = $dto->provider_key . '|' . $dto->provider_id;
150+
if (!in_array($key, $seenKeys)) {
151+
$seenKeys[] = $key;
152+
$uniqueDtos[] = $dto;
153+
}
154+
}
155+
156+
// Convert DTOs to result format
157+
$partResult['search_results'] = array_map(
158+
fn($dto) => ['dto' => $dto, 'localPart' => $this->existingPartFinder->findFirstExisting($dto)],
159+
$uniqueDtos
160+
);
161+
162+
$searchResults[] = $partResult;
163+
}
164+
}
165+
166+
return $this->render('info_providers/bulk_import/step1.html.twig', [
167+
'form' => $form,
168+
'parts' => $parts,
169+
'search_results' => $searchResults,
170+
'fieldChoices' => $fieldChoices
171+
]);
172+
}
173+
174+
private function getKeywordFromField(Part $part, string $field): ?string
175+
{
176+
return match ($field) {
177+
'mpn' => $part->getManufacturerProductNumber(),
178+
'name' => $part->getName(),
179+
default => $this->getSupplierPartNumber($part, $field)
180+
};
181+
}
182+
183+
private function getSupplierPartNumber(Part $part, string $field): ?string
184+
{
185+
// Check if this is a supplier SPN field
186+
if (!str_ends_with($field, '_spn')) {
187+
return null;
188+
}
189+
190+
// Extract supplier key (remove _spn suffix)
191+
$supplierKey = substr($field, 0, -4);
192+
193+
// Get all suppliers to find matching one
194+
$suppliers = $this->entityManager->getRepository(Supplier::class)->findAll();
195+
196+
foreach ($suppliers as $supplier) {
197+
$normalizedName = strtolower(str_replace([' ', '-', '_'], '_', $supplier->getName()));
198+
if ($normalizedName === $supplierKey) {
199+
// Find order detail for this supplier
200+
$orderDetail = $part->getOrderdetails()->filter(
201+
fn($od) => $od->getSupplier()?->getId() === $supplier->getId()
202+
)->first();
203+
204+
return $orderDetail ? $orderDetail->getSupplierpartnr() : null;
205+
}
206+
}
207+
208+
return null;
209+
}
210+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace App\Form\InfoProviderSystem;
24+
25+
use App\Entity\Parts\Part;
26+
use Symfony\Component\Form\AbstractType;
27+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
28+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
29+
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
30+
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
31+
use Symfony\Component\Form\FormBuilderInterface;
32+
use Symfony\Component\OptionsResolver\OptionsResolver;
33+
34+
class BulkProviderSearchType extends AbstractType
35+
{
36+
public function buildForm(FormBuilderInterface $builder, array $options): void
37+
{
38+
$parts = $options['parts'];
39+
40+
$builder->add('part_configurations', CollectionType::class, [
41+
'entry_type' => PartProviderConfigurationType::class,
42+
'entry_options' => [
43+
'label' => false,
44+
],
45+
'allow_add' => false,
46+
'allow_delete' => false,
47+
'label' => false,
48+
]);
49+
50+
$builder->add('submit', SubmitType::class, [
51+
'label' => 'info_providers.bulk_search.submit'
52+
]);
53+
}
54+
55+
public function configureOptions(OptionsResolver $resolver): void
56+
{
57+
$resolver->setDefaults([
58+
'parts' => [],
59+
]);
60+
$resolver->setRequired('parts');
61+
}
62+
63+
private function getDefaultSearchField(Part $part): string
64+
{
65+
// Default to MPN if available, otherwise name
66+
return $part->getManufacturerProductNumber() ? 'mpn' : 'name';
67+
}
68+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace App\Form\InfoProviderSystem;
24+
25+
use Symfony\Component\Form\AbstractType;
26+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
27+
use Symfony\Component\Form\FormBuilderInterface;
28+
use Symfony\Component\OptionsResolver\OptionsResolver;
29+
30+
class FieldToProviderMappingType extends AbstractType
31+
{
32+
public function buildForm(FormBuilderInterface $builder, array $options): void
33+
{
34+
$fieldChoices = $options['field_choices'] ?? [];
35+
36+
$builder->add('field', ChoiceType::class, [
37+
'label' => 'info_providers.bulk_search.search_field',
38+
'choices' => $fieldChoices,
39+
'expanded' => false,
40+
'multiple' => false,
41+
'required' => false,
42+
'placeholder' => 'info_providers.bulk_search.field.select',
43+
]);
44+
45+
$builder->add('providers', ProviderSelectType::class, [
46+
'label' => 'info_providers.bulk_search.providers',
47+
'help' => 'info_providers.bulk_search.providers.help',
48+
'required' => false,
49+
]);
50+
}
51+
52+
public function configureOptions(OptionsResolver $resolver): void
53+
{
54+
$resolver->setDefaults([
55+
'field_choices' => [],
56+
]);
57+
}
58+
}

0 commit comments

Comments
 (0)