2022-12-18 21:58:21 +01:00
|
|
|
<?php
|
2023-06-11 18:59:07 +02:00
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2022-12-18 21:58:21 +01:00
|
|
|
/*
|
|
|
|
|
* 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\Controller;
|
|
|
|
|
|
|
|
|
|
use App\DataTables\ProjectBomEntriesDataTable;
|
2022-12-28 23:32:46 +01:00
|
|
|
use App\Entity\Parts\Part;
|
2022-12-18 21:58:21 +01:00
|
|
|
use App\Entity\ProjectSystem\Project;
|
2022-12-28 23:32:46 +01:00
|
|
|
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
2023-07-03 00:28:37 +02:00
|
|
|
use App\Form\ProjectSystem\ProjectAddPartsType;
|
2023-01-22 00:01:16 +01:00
|
|
|
use App\Form\ProjectSystem\ProjectBuildType;
|
|
|
|
|
use App\Helpers\Projects\ProjectBuildRequest;
|
2023-03-16 00:05:46 +01:00
|
|
|
use App\Services\ImportExportSystem\BOMImporter;
|
2023-01-18 23:07:51 +01:00
|
|
|
use App\Services\ProjectSystem\ProjectBuildHelper;
|
2024-08-21 22:15:26 +02:00
|
|
|
use App\Settings\BehaviorSettings\TableSettings;
|
2022-12-28 23:32:46 +01:00
|
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
2023-03-16 23:32:12 +01:00
|
|
|
use League\Csv\SyntaxError;
|
2022-12-18 21:58:21 +01:00
|
|
|
use Omines\DataTablesBundle\DataTableFactory;
|
2025-08-03 18:46:46 +02:00
|
|
|
use Psr\Log\LoggerInterface;
|
2022-12-18 21:58:21 +01:00
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
2023-03-16 00:05:46 +01:00
|
|
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
|
|
|
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
|
|
|
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
2022-12-28 23:32:46 +01:00
|
|
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
2022-12-18 21:58:21 +01:00
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
2022-12-28 23:32:46 +01:00
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
2024-03-03 20:37:33 +01:00
|
|
|
use Symfony\Component\Routing\Attribute\Route;
|
2023-03-16 00:05:46 +01:00
|
|
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
|
|
|
|
|
|
|
|
|
use function Symfony\Component\Translation\t;
|
2022-12-18 21:58:21 +01:00
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
#[Route(path: '/project')]
|
2022-12-18 21:58:21 +01:00
|
|
|
class ProjectController extends AbstractController
|
|
|
|
|
{
|
2023-06-11 14:15:46 +02:00
|
|
|
public function __construct(private readonly DataTableFactory $dataTableFactory)
|
2022-12-18 21:58:21 +01:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
#[Route(path: '/{id}/info', name: 'project_info', requirements: ['id' => '\d+'])]
|
2024-08-21 22:15:26 +02:00
|
|
|
public function info(Project $project, Request $request, ProjectBuildHelper $buildHelper, TableSettings $tableSettings): Response
|
2022-12-18 21:58:21 +01:00
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('read', $project);
|
|
|
|
|
|
2024-08-21 22:15:26 +02:00
|
|
|
$table = $this->dataTableFactory->createFromType(ProjectBomEntriesDataTable::class, ['project' => $project],
|
|
|
|
|
['pageLength' => $tableSettings->fullDefaultPageSize])
|
2022-12-18 21:58:21 +01:00
|
|
|
->handleRequest($request);
|
|
|
|
|
|
|
|
|
|
if ($table->isCallback()) {
|
|
|
|
|
return $table->getResponse();
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-04 23:03:32 +01:00
|
|
|
return $this->render('projects/info/info.html.twig', [
|
2023-01-18 23:07:51 +01:00
|
|
|
'buildHelper' => $buildHelper,
|
2022-12-18 21:58:21 +01:00
|
|
|
'datatable' => $table,
|
|
|
|
|
'project' => $project,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2022-12-28 23:32:46 +01:00
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
#[Route(path: '/{id}/build', name: 'project_build', requirements: ['id' => '\d+'])]
|
2023-01-22 16:59:58 +01:00
|
|
|
public function build(Project $project, Request $request, ProjectBuildHelper $buildHelper, EntityManagerInterface $entityManager): Response
|
2023-01-21 21:41:08 +01:00
|
|
|
{
|
|
|
|
|
$this->denyAccessUnlessGranted('read', $project);
|
|
|
|
|
|
|
|
|
|
//If no number of builds is given (or it is invalid), just assume 1
|
|
|
|
|
$number_of_builds = $request->query->getInt('n', 1);
|
|
|
|
|
if ($number_of_builds < 1) {
|
|
|
|
|
$number_of_builds = 1;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-22 00:01:16 +01:00
|
|
|
$projectBuildRequest = new ProjectBuildRequest($project, $number_of_builds);
|
|
|
|
|
$form = $this->createForm(ProjectBuildType::class, $projectBuildRequest);
|
2023-01-21 21:41:08 +01:00
|
|
|
|
2023-01-22 00:01:16 +01:00
|
|
|
$form->handleRequest($request);
|
2023-01-22 16:59:58 +01:00
|
|
|
if ($form->isSubmitted()) {
|
|
|
|
|
if ($form->isValid()) {
|
2023-01-22 23:40:10 +01:00
|
|
|
//Ensure that the user can withdraw stock from all parts
|
|
|
|
|
$this->denyAccessUnlessGranted('@parts_stock.withdraw');
|
|
|
|
|
|
2023-01-22 23:27:45 +01:00
|
|
|
//We have to do a flush already here, so that the newly created partLot gets an ID and can be logged to DB later.
|
|
|
|
|
$entityManager->flush();
|
|
|
|
|
$buildHelper->doBuild($projectBuildRequest);
|
2023-01-22 16:59:58 +01:00
|
|
|
$entityManager->flush();
|
|
|
|
|
$this->addFlash('success', 'project.build.flash.success');
|
|
|
|
|
|
|
|
|
|
return $this->redirect(
|
2025-08-03 18:46:46 +02:00
|
|
|
$request->get(
|
|
|
|
|
'_redirect',
|
|
|
|
|
$this->generateUrl(
|
|
|
|
|
'project_info',
|
|
|
|
|
['id' => $project->getID()]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
);
|
2023-01-22 16:59:58 +01:00
|
|
|
}
|
2023-04-15 22:05:29 +02:00
|
|
|
|
|
|
|
|
$this->addFlash('error', 'project.build.flash.invalid_input');
|
2023-01-22 00:01:16 +01:00
|
|
|
}
|
|
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
return $this->render('projects/build/build.html.twig', [
|
2023-01-21 21:41:08 +01:00
|
|
|
'buildHelper' => $buildHelper,
|
|
|
|
|
'project' => $project,
|
2023-01-22 00:01:16 +01:00
|
|
|
'build_request' => $projectBuildRequest,
|
2023-01-21 21:41:08 +01:00
|
|
|
'number_of_builds' => $number_of_builds,
|
2023-01-22 00:01:16 +01:00
|
|
|
'form' => $form,
|
2023-01-21 21:41:08 +01:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
#[Route(path: '/{id}/import_bom', name: 'project_import_bom', requirements: ['id' => '\d+'])]
|
2025-08-03 18:46:46 +02:00
|
|
|
public function importBOM(
|
|
|
|
|
Request $request,
|
|
|
|
|
EntityManagerInterface $entityManager,
|
|
|
|
|
Project $project,
|
|
|
|
|
BOMImporter $BOMImporter,
|
|
|
|
|
ValidatorInterface $validator
|
|
|
|
|
): Response {
|
2023-03-16 00:05:46 +01:00
|
|
|
$this->denyAccessUnlessGranted('edit', $project);
|
|
|
|
|
|
|
|
|
|
$builder = $this->createFormBuilder();
|
|
|
|
|
$builder->add('file', FileType::class, [
|
|
|
|
|
'label' => 'import.file',
|
|
|
|
|
'required' => true,
|
|
|
|
|
'attr' => [
|
|
|
|
|
'accept' => '.csv'
|
|
|
|
|
]
|
|
|
|
|
]);
|
|
|
|
|
$builder->add('type', ChoiceType::class, [
|
|
|
|
|
'label' => 'project.bom_import.type',
|
|
|
|
|
'required' => true,
|
|
|
|
|
'choices' => [
|
|
|
|
|
'project.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew',
|
2025-08-03 18:46:46 +02:00
|
|
|
'project.bom_import.type.kicad_schematic' => 'kicad_schematic',
|
2023-03-16 00:05:46 +01:00
|
|
|
]
|
|
|
|
|
]);
|
|
|
|
|
$builder->add('clear_existing_bom', CheckboxType::class, [
|
|
|
|
|
'label' => 'project.bom_import.clear_existing_bom',
|
|
|
|
|
'required' => false,
|
|
|
|
|
'data' => false,
|
|
|
|
|
'help' => 'project.bom_import.clear_existing_bom.help',
|
|
|
|
|
]);
|
|
|
|
|
$builder->add('submit', SubmitType::class, [
|
|
|
|
|
'label' => 'import.btn',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$form = $builder->getForm();
|
|
|
|
|
|
|
|
|
|
$form->handleRequest($request);
|
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
|
|
|
|
|
|
|
|
//Clear existing BOM entries if requested
|
|
|
|
|
if ($form->get('clear_existing_bom')->getData()) {
|
|
|
|
|
$project->getBomEntries()->clear();
|
2023-03-17 00:11:01 +01:00
|
|
|
$entityManager->flush();
|
2023-03-16 00:05:46 +01:00
|
|
|
}
|
|
|
|
|
|
2025-08-03 18:46:46 +02:00
|
|
|
$import_type = $form->get('type')->getData();
|
|
|
|
|
|
2023-03-16 00:05:46 +01:00
|
|
|
try {
|
2025-08-03 18:46:46 +02:00
|
|
|
// For schematic imports, redirect to field mapping step
|
|
|
|
|
if ($import_type === 'kicad_schematic') {
|
|
|
|
|
// Store file content and options in session for field mapping step
|
|
|
|
|
$file_content = $form->get('file')->getData()->getContent();
|
|
|
|
|
$clear_existing = $form->get('clear_existing_bom')->getData();
|
|
|
|
|
|
|
|
|
|
$request->getSession()->set('bom_import_data', $file_content);
|
|
|
|
|
$request->getSession()->set('bom_import_clear', $clear_existing);
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('project_import_bom_map_fields', ['id' => $project->getID()]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For PCB imports, proceed directly
|
2023-03-16 00:05:46 +01:00
|
|
|
$entries = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [
|
2025-08-03 18:46:46 +02:00
|
|
|
'type' => $import_type,
|
2023-03-16 00:05:46 +01:00
|
|
|
]);
|
|
|
|
|
|
2025-08-03 18:46:46 +02:00
|
|
|
// Validate the project entries
|
2023-03-16 00:05:46 +01:00
|
|
|
$errors = $validator->validateProperty($project, 'bom_entries');
|
|
|
|
|
|
2025-08-03 18:46:46 +02:00
|
|
|
// If no validation errors occurred, save the changes and redirect to edit page
|
|
|
|
|
if (count($errors) === 0) {
|
2023-03-16 00:05:46 +01:00
|
|
|
$this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)]));
|
|
|
|
|
$entityManager->flush();
|
|
|
|
|
return $this->redirectToRoute('project_edit', ['id' => $project->getID()]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-03 18:46:46 +02:00
|
|
|
// When we get here, there were validation errors
|
2023-06-13 20:24:54 +02:00
|
|
|
$this->addFlash('error', t('project.bom_import.flash.invalid_entries'));
|
|
|
|
|
|
2025-08-03 18:46:46 +02:00
|
|
|
} catch (\UnexpectedValueException | SyntaxError $e) {
|
2023-03-16 23:32:12 +01:00
|
|
|
$this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()]));
|
2023-03-16 00:05:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
return $this->render('projects/import_bom.html.twig', [
|
2023-03-16 00:05:46 +01:00
|
|
|
'project' => $project,
|
|
|
|
|
'form' => $form,
|
|
|
|
|
'errors' => $errors ?? null,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-03 18:46:46 +02:00
|
|
|
#[Route(path: '/{id}/import_bom/map_fields', name: 'project_import_bom_map_fields', requirements: ['id' => '\d+'])]
|
|
|
|
|
public function importBOMMapFields(
|
|
|
|
|
Request $request,
|
|
|
|
|
EntityManagerInterface $entityManager,
|
|
|
|
|
Project $project,
|
|
|
|
|
BOMImporter $BOMImporter,
|
|
|
|
|
ValidatorInterface $validator,
|
|
|
|
|
LoggerInterface $logger
|
|
|
|
|
): Response {
|
|
|
|
|
$this->denyAccessUnlessGranted('edit', $project);
|
|
|
|
|
|
|
|
|
|
// Get stored data from session
|
|
|
|
|
$file_content = $request->getSession()->get('bom_import_data');
|
|
|
|
|
$clear_existing = $request->getSession()->get('bom_import_clear', false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!$file_content) {
|
|
|
|
|
$this->addFlash('error', 'project.bom_import.flash.session_expired');
|
|
|
|
|
return $this->redirectToRoute('project_import_bom', ['id' => $project->getID()]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detect fields and get suggestions
|
|
|
|
|
$detected_fields = $BOMImporter->detectFields($file_content);
|
|
|
|
|
$suggested_mapping = $BOMImporter->getSuggestedFieldMapping($detected_fields);
|
|
|
|
|
|
|
|
|
|
// Create mapping of original field names to sanitized field names for template
|
|
|
|
|
$field_name_mapping = [];
|
|
|
|
|
foreach ($detected_fields as $field) {
|
|
|
|
|
$sanitized_field = preg_replace('/[^a-zA-Z0-9_-]/', '_', $field);
|
|
|
|
|
$field_name_mapping[$field] = $sanitized_field;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create form for field mapping
|
|
|
|
|
$builder = $this->createFormBuilder();
|
|
|
|
|
|
|
|
|
|
// Add delimiter selection
|
|
|
|
|
$builder->add('delimiter', ChoiceType::class, [
|
|
|
|
|
'label' => 'project.bom_import.delimiter',
|
|
|
|
|
'required' => true,
|
|
|
|
|
'data' => ',',
|
|
|
|
|
'choices' => [
|
|
|
|
|
'project.bom_import.delimiter.comma' => ',',
|
|
|
|
|
'project.bom_import.delimiter.semicolon' => ';',
|
|
|
|
|
'project.bom_import.delimiter.tab' => "\t",
|
|
|
|
|
]
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Get dynamic field mapping targets from BOMImporter
|
|
|
|
|
$available_targets = $BOMImporter->getAvailableFieldTargets();
|
|
|
|
|
$target_fields = ['project.bom_import.field_mapping.ignore' => ''];
|
|
|
|
|
|
|
|
|
|
foreach ($available_targets as $target_key => $target_info) {
|
|
|
|
|
$target_fields[$target_info['label']] = $target_key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($detected_fields as $field) {
|
|
|
|
|
// Sanitize field name for form use - replace invalid characters with underscores
|
|
|
|
|
$sanitized_field = preg_replace('/[^a-zA-Z0-9_-]/', '_', $field);
|
|
|
|
|
$builder->add('mapping_' . $sanitized_field, ChoiceType::class, [
|
|
|
|
|
'label' => $field,
|
|
|
|
|
'required' => false,
|
|
|
|
|
'choices' => $target_fields,
|
|
|
|
|
'data' => $suggested_mapping[$field] ?? '',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$builder->add('submit', SubmitType::class, [
|
|
|
|
|
'label' => 'project.bom_import.preview',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$form = $builder->getForm();
|
|
|
|
|
$form->handleRequest($request);
|
|
|
|
|
|
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
|
|
|
// Build field mapping array with priority support
|
|
|
|
|
$field_mapping = [];
|
|
|
|
|
$field_priorities = [];
|
|
|
|
|
$delimiter = $form->get('delimiter')->getData();
|
|
|
|
|
|
|
|
|
|
foreach ($detected_fields as $field) {
|
|
|
|
|
$sanitized_field = preg_replace('/[^a-zA-Z0-9_-]/', '_', $field);
|
|
|
|
|
$target = $form->get('mapping_' . $sanitized_field)->getData();
|
|
|
|
|
if (!empty($target)) {
|
|
|
|
|
$field_mapping[$field] = $target;
|
|
|
|
|
|
|
|
|
|
// Get priority from request (default to 10)
|
|
|
|
|
$priority = $request->request->get('priority_' . $sanitized_field, 10);
|
|
|
|
|
$field_priorities[$field] = (int) $priority;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate field mapping
|
|
|
|
|
$validation = $BOMImporter->validateFieldMapping($field_mapping, $detected_fields);
|
|
|
|
|
|
|
|
|
|
if (!$validation['is_valid']) {
|
|
|
|
|
foreach ($validation['errors'] as $error) {
|
|
|
|
|
$this->addFlash('error', $error);
|
|
|
|
|
}
|
|
|
|
|
foreach ($validation['warnings'] as $warning) {
|
|
|
|
|
$this->addFlash('warning', $warning);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->render('projects/import_bom_map_fields.html.twig', [
|
|
|
|
|
'project' => $project,
|
|
|
|
|
'form' => $form->createView(),
|
|
|
|
|
'detected_fields' => $detected_fields,
|
|
|
|
|
'suggested_mapping' => $suggested_mapping,
|
|
|
|
|
'field_name_mapping' => $field_name_mapping,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show warnings but continue
|
|
|
|
|
foreach ($validation['warnings'] as $warning) {
|
|
|
|
|
$this->addFlash('warning', $warning);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Re-detect fields with chosen delimiter
|
|
|
|
|
$detected_fields = $BOMImporter->detectFields($file_content, $delimiter);
|
|
|
|
|
|
|
|
|
|
// Clear existing BOM entries if requested
|
|
|
|
|
if ($clear_existing) {
|
|
|
|
|
$existing_count = $project->getBomEntries()->count();
|
|
|
|
|
$logger->info('Clearing existing BOM entries', [
|
|
|
|
|
'existing_count' => $existing_count,
|
|
|
|
|
'project_id' => $project->getID(),
|
|
|
|
|
]);
|
|
|
|
|
$project->getBomEntries()->clear();
|
|
|
|
|
$entityManager->flush();
|
|
|
|
|
$logger->info('Existing BOM entries cleared');
|
|
|
|
|
} else {
|
|
|
|
|
$existing_count = $project->getBomEntries()->count();
|
|
|
|
|
$logger->info('Keeping existing BOM entries', [
|
|
|
|
|
'existing_count' => $existing_count,
|
|
|
|
|
'project_id' => $project->getID(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate data before importing
|
|
|
|
|
$validation_result = $BOMImporter->validateBOMData($file_content, [
|
|
|
|
|
'type' => 'kicad_schematic',
|
|
|
|
|
'field_mapping' => $field_mapping,
|
|
|
|
|
'field_priorities' => $field_priorities,
|
|
|
|
|
'delimiter' => $delimiter,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Log validation results
|
|
|
|
|
$logger->info('BOM import validation completed', [
|
|
|
|
|
'total_entries' => $validation_result['total_entries'],
|
|
|
|
|
'valid_entries' => $validation_result['valid_entries'],
|
|
|
|
|
'invalid_entries' => $validation_result['invalid_entries'],
|
|
|
|
|
'error_count' => count($validation_result['errors']),
|
|
|
|
|
'warning_count' => count($validation_result['warnings']),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Show validation warnings to user
|
|
|
|
|
foreach ($validation_result['warnings'] as $warning) {
|
|
|
|
|
$this->addFlash('warning', $warning);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If there are validation errors, show them and stop
|
2025-09-06 23:10:47 +02:00
|
|
|
if (!empty($validation_result['errors'])) {
|
2025-08-03 18:46:46 +02:00
|
|
|
foreach ($validation_result['errors'] as $error) {
|
|
|
|
|
$this->addFlash('error', $error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->render('projects/import_bom_map_fields.html.twig', [
|
|
|
|
|
'project' => $project,
|
|
|
|
|
'form' => $form->createView(),
|
|
|
|
|
'detected_fields' => $detected_fields,
|
|
|
|
|
'suggested_mapping' => $suggested_mapping,
|
|
|
|
|
'field_name_mapping' => $field_name_mapping,
|
|
|
|
|
'validation_result' => $validation_result,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Import with field mapping and priorities (validation already passed)
|
|
|
|
|
$entries = $BOMImporter->stringToBOMEntries($file_content, [
|
|
|
|
|
'type' => 'kicad_schematic',
|
|
|
|
|
'field_mapping' => $field_mapping,
|
|
|
|
|
'field_priorities' => $field_priorities,
|
|
|
|
|
'delimiter' => $delimiter,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Log entry details for debugging
|
|
|
|
|
$logger->info('BOM entries created', [
|
|
|
|
|
'total_entries' => count($entries),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
foreach ($entries as $index => $entry) {
|
|
|
|
|
$logger->debug("BOM entry {$index}", [
|
|
|
|
|
'name' => $entry->getName(),
|
|
|
|
|
'mountnames' => $entry->getMountnames(),
|
|
|
|
|
'quantity' => $entry->getQuantity(),
|
|
|
|
|
'comment' => $entry->getComment(),
|
|
|
|
|
'part_id' => $entry->getPart()?->getID(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assign entries to project
|
|
|
|
|
$logger->info('Adding BOM entries to project', [
|
|
|
|
|
'entries_count' => count($entries),
|
|
|
|
|
'project_id' => $project->getID(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
foreach ($entries as $index => $entry) {
|
|
|
|
|
$logger->debug("Adding BOM entry {$index} to project", [
|
|
|
|
|
'name' => $entry->getName(),
|
|
|
|
|
'part_id' => $entry->getPart()?->getID(),
|
|
|
|
|
'quantity' => $entry->getQuantity(),
|
|
|
|
|
]);
|
|
|
|
|
$project->addBomEntry($entry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate the project entries (includes collection constraints)
|
|
|
|
|
$errors = $validator->validateProperty($project, 'bom_entries');
|
|
|
|
|
|
|
|
|
|
// If no validation errors occurred, save and redirect
|
|
|
|
|
if (count($errors) === 0) {
|
|
|
|
|
$this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)]));
|
|
|
|
|
$entityManager->flush();
|
|
|
|
|
|
|
|
|
|
// Clear session data
|
|
|
|
|
$request->getSession()->remove('bom_import_data');
|
|
|
|
|
$request->getSession()->remove('bom_import_clear');
|
|
|
|
|
|
|
|
|
|
return $this->redirectToRoute('project_edit', ['id' => $project->getID()]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When we get here, there were validation errors
|
|
|
|
|
$this->addFlash('error', t('project.bom_import.flash.invalid_entries'));
|
|
|
|
|
|
2025-09-06 23:10:47 +02:00
|
|
|
//Print validation errors to log for debugging
|
|
|
|
|
foreach ($errors as $error) {
|
|
|
|
|
$logger->error('BOM entry validation error', [
|
|
|
|
|
'message' => $error->getMessage(),
|
|
|
|
|
'invalid_value' => $error->getInvalidValue(),
|
|
|
|
|
]);
|
|
|
|
|
//And show as flash message
|
|
|
|
|
$this->addFlash('error', $error->getMessage(),);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-03 18:46:46 +02:00
|
|
|
} catch (\UnexpectedValueException | SyntaxError $e) {
|
|
|
|
|
$this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()]));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->render('projects/import_bom_map_fields.html.twig', [
|
|
|
|
|
'project' => $project,
|
|
|
|
|
'form' => $form,
|
|
|
|
|
'detected_fields' => $detected_fields,
|
|
|
|
|
'suggested_mapping' => $suggested_mapping,
|
|
|
|
|
'field_name_mapping' => $field_name_mapping,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
#[Route(path: '/add_parts', name: 'project_add_parts_no_id')]
|
|
|
|
|
#[Route(path: '/{id}/add_parts', name: 'project_add_parts', requirements: ['id' => '\d+'])]
|
2022-12-29 13:15:26 +01:00
|
|
|
public function addPart(Request $request, EntityManagerInterface $entityManager, ?Project $project): Response
|
2022-12-28 23:32:46 +01:00
|
|
|
{
|
2025-08-03 18:46:46 +02:00
|
|
|
if ($project instanceof Project) {
|
2022-12-29 13:15:26 +01:00
|
|
|
$this->denyAccessUnlessGranted('edit', $project);
|
|
|
|
|
} else {
|
2023-01-08 20:10:58 +01:00
|
|
|
$this->denyAccessUnlessGranted('@projects.edit');
|
2022-12-29 13:15:26 +01:00
|
|
|
}
|
2022-12-28 23:32:46 +01:00
|
|
|
|
2023-07-03 00:28:37 +02:00
|
|
|
$form = $this->createForm(ProjectAddPartsType::class, null, [
|
|
|
|
|
'project' => $project,
|
2022-12-28 23:32:46 +01:00
|
|
|
]);
|
2023-07-03 00:28:37 +02:00
|
|
|
|
2022-12-28 23:32:46 +01:00
|
|
|
|
|
|
|
|
//Preset the BOM entries with the selected parts, when the form was not submitted yet
|
|
|
|
|
$preset_data = new ArrayCollection();
|
2023-06-11 14:15:46 +02:00
|
|
|
foreach (explode(',', (string) $request->get('parts', '')) as $part_id) {
|
2024-06-13 22:19:17 +02:00
|
|
|
//Skip empty part IDs. Postgres seems to be especially sensitive to empty strings, as it does not allow them in integer columns
|
|
|
|
|
if ($part_id === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-28 23:32:46 +01:00
|
|
|
$part = $entityManager->getRepository(Part::class)->find($part_id);
|
|
|
|
|
if (null !== $part) {
|
|
|
|
|
//If there is already a BOM entry for this part, we use this one (we edit it then)
|
|
|
|
|
$bom_entry = $entityManager->getRepository(ProjectBOMEntry::class)->findOneBy([
|
|
|
|
|
'project' => $project,
|
|
|
|
|
'part' => $part
|
|
|
|
|
]);
|
2024-06-22 00:31:43 +02:00
|
|
|
if ($bom_entry !== null) {
|
2022-12-28 23:32:46 +01:00
|
|
|
$preset_data->add($bom_entry);
|
|
|
|
|
} else { //Otherwise create an empty one
|
|
|
|
|
$entry = new ProjectBOMEntry();
|
|
|
|
|
$entry->setProject($project);
|
|
|
|
|
$entry->setPart($part);
|
|
|
|
|
$preset_data->add($entry);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$form['bom_entries']->setData($preset_data);
|
|
|
|
|
|
|
|
|
|
$form->handleRequest($request);
|
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
2022-12-29 13:15:26 +01:00
|
|
|
$target_project = $project ?? $form->get('project')->getData();
|
|
|
|
|
|
|
|
|
|
//Ensure that we really have acces to the selected project
|
|
|
|
|
$this->denyAccessUnlessGranted('edit', $target_project);
|
|
|
|
|
|
2022-12-28 23:32:46 +01:00
|
|
|
$data = $form->getData();
|
|
|
|
|
$bom_entries = $data['bom_entries'];
|
2025-08-03 18:46:46 +02:00
|
|
|
foreach ($bom_entries as $bom_entry) {
|
2022-12-29 13:15:26 +01:00
|
|
|
$target_project->addBOMEntry($bom_entry);
|
2022-12-28 23:32:46 +01:00
|
|
|
}
|
2023-07-03 00:28:37 +02:00
|
|
|
|
|
|
|
|
|
2022-12-28 23:32:46 +01:00
|
|
|
$entityManager->flush();
|
|
|
|
|
|
2023-07-03 00:28:37 +02:00
|
|
|
|
2022-12-28 23:32:46 +01:00
|
|
|
//If a redirect query parameter is set, redirect to this page
|
|
|
|
|
if ($request->query->get('_redirect')) {
|
|
|
|
|
return $this->redirect($request->query->get('_redirect'));
|
|
|
|
|
}
|
|
|
|
|
//Otherwise just show the project info page
|
2022-12-29 13:15:26 +01:00
|
|
|
return $this->redirectToRoute('project_info', ['id' => $target_project->getID()]);
|
2022-12-28 23:32:46 +01:00
|
|
|
}
|
|
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
return $this->render('projects/add_parts.html.twig', [
|
2022-12-28 23:32:46 +01:00
|
|
|
'project' => $project,
|
|
|
|
|
'form' => $form,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2023-06-11 18:59:07 +02:00
|
|
|
}
|