mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-12 21:29:33 +00:00
Assemblies einführen
This commit is contained in:
parent
e1418dfdc1
commit
6fa960df42
107 changed files with 14101 additions and 96 deletions
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Command\Migrations;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
|
|
@ -88,6 +89,7 @@ class ConvertBBCodeCommand extends Command
|
|||
AttachmentType::class => ['comment'],
|
||||
StorageLocation::class => ['comment'],
|
||||
Project::class => ['comment'],
|
||||
Assembly::class => ['comment'],
|
||||
Category::class => ['comment'],
|
||||
Manufacturer::class => ['comment'],
|
||||
MeasurementUnit::class => ['comment'],
|
||||
|
|
|
|||
80
src/Controller/AdminPages/AssemblyAdminController.php
Normal file
80
src/Controller/AdminPages/AssemblyAdminController.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?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\Controller\AdminPages;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Attachments\AssemblyAttachment;
|
||||
use App\Entity\Parameters\AssemblyParameter;
|
||||
use App\Form\AdminPages\AssemblyAdminForm;
|
||||
use App\Services\ImportExportSystem\EntityExporter;
|
||||
use App\Services\ImportExportSystem\EntityImporter;
|
||||
use App\Services\Trees\StructuralElementRecursionHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route(path: '/assembly')]
|
||||
class AssemblyAdminController extends BaseAdminController
|
||||
{
|
||||
protected string $entity_class = Assembly::class;
|
||||
protected string $twig_template = 'admin/assembly_admin.html.twig';
|
||||
protected string $form_class = AssemblyAdminForm::class;
|
||||
protected string $route_base = 'assembly';
|
||||
protected string $attachment_class = AssemblyAttachment::class;
|
||||
protected ?string $parameter_class = AssemblyParameter::class;
|
||||
|
||||
#[Route(path: '/{id}', name: 'assembly_delete', methods: ['DELETE'])]
|
||||
public function delete(Request $request, Assembly $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
|
||||
{
|
||||
return $this->_delete($request, $entity, $recursionHelper);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/edit/{timestamp}', name: 'assembly_edit', requirements: ['id' => '\d+'])]
|
||||
#[Route(path: '/{id}/edit', requirements: ['id' => '\d+'])]
|
||||
public function edit(Assembly $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
|
||||
{
|
||||
return $this->_edit($entity, $request, $em, $timestamp);
|
||||
}
|
||||
|
||||
#[Route(path: '/new', name: 'assembly_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'assembly_clone')]
|
||||
#[Route(path: '/')]
|
||||
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Assembly $entity = null): Response
|
||||
{
|
||||
return $this->_new($request, $em, $importer, $entity);
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'assembly_export_all')]
|
||||
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportAll($em, $exporter, $request);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/export', name: 'assembly_export')]
|
||||
public function exportEntity(Assembly $entity, EntityExporter $exporter, Request $request): Response
|
||||
{
|
||||
return $this->_exportEntity($entity, $exporter, $request);
|
||||
}
|
||||
}
|
||||
302
src/Controller/AssemblyController.php
Normal file
302
src/Controller/AssemblyController.php
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
<?php
|
||||
|
||||
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\Controller;
|
||||
|
||||
use App\DataTables\AssemblyBomEntriesDataTable;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Form\AssemblySystem\AssemblyAddPartsType;
|
||||
use App\Form\AssemblySystem\AssemblyBuildType;
|
||||
use App\Helpers\Assemblies\AssemblyBuildRequest;
|
||||
use App\Repository\PartRepository;
|
||||
use App\Services\ImportExportSystem\BOMImporter;
|
||||
use App\Services\AssemblySystem\AssemblyBuildHelper;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use League\Csv\SyntaxError;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
#[Route(path: '/assembly')]
|
||||
class AssemblyController extends AbstractController
|
||||
{
|
||||
private PartRepository $partRepository;
|
||||
|
||||
public function __construct(
|
||||
private readonly DataTableFactory $dataTableFactory,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
$this->partRepository = $this->entityManager->getRepository(Part::class);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])]
|
||||
public function info(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $assembly);
|
||||
|
||||
$table = $this->dataTableFactory->createFromType(AssemblyBomEntriesDataTable::class, ['assembly' => $assembly])
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
return $table->getResponse();
|
||||
}
|
||||
|
||||
return $this->render('assemblies/info/info.html.twig', [
|
||||
'buildHelper' => $buildHelper,
|
||||
'datatable' => $table,
|
||||
'assembly' => $assembly,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/build', name: 'assembly_build', requirements: ['id' => '\d+'])]
|
||||
public function build(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper, EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $assembly);
|
||||
|
||||
//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;
|
||||
}
|
||||
|
||||
$assemblyBuildRequest = new AssemblyBuildRequest($assembly, $number_of_builds);
|
||||
$form = $this->createForm(AssemblyBuildType::class, $assemblyBuildRequest);
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted()) {
|
||||
if ($form->isValid()) {
|
||||
//Ensure that the user can withdraw stock from all parts
|
||||
$this->denyAccessUnlessGranted('@parts_stock.withdraw');
|
||||
|
||||
//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($assemblyBuildRequest);
|
||||
$entityManager->flush();
|
||||
$this->addFlash('success', 'assembly.build.flash.success');
|
||||
|
||||
return $this->redirect(
|
||||
$request->get('_redirect',
|
||||
$this->generateUrl('assembly_info', ['id' => $assembly->getID()]
|
||||
)));
|
||||
}
|
||||
|
||||
$this->addFlash('error', 'assembly.build.flash.invalid_input');
|
||||
}
|
||||
|
||||
return $this->render('assemblies/build/build.html.twig', [
|
||||
'buildHelper' => $buildHelper,
|
||||
'assembly' => $assembly,
|
||||
'build_request' => $assemblyBuildRequest,
|
||||
'number_of_builds' => $number_of_builds,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/import_bom', name: 'assembly_import_bom', requirements: ['id' => '\d+'])]
|
||||
public function importBOM(Request $request, EntityManagerInterface $entityManager, Assembly $assembly,
|
||||
BOMImporter $BOMImporter, ValidatorInterface $validator): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('edit', $assembly);
|
||||
|
||||
$builder = $this->createFormBuilder();
|
||||
$builder->add('file', FileType::class, [
|
||||
'label' => 'import.file',
|
||||
'required' => true,
|
||||
'attr' => [
|
||||
'accept' => '.csv, .json'
|
||||
]
|
||||
]);
|
||||
$builder->add('type', ChoiceType::class, [
|
||||
'label' => 'assembly.bom_import.type',
|
||||
'required' => true,
|
||||
'choices' => [
|
||||
'assembly.bom_import.type.json' => 'json',
|
||||
'assembly.bom_import.type.kicad_pcbnew' => 'kicad_pcbnew',
|
||||
]
|
||||
]);
|
||||
$builder->add('clear_existing_bom', CheckboxType::class, [
|
||||
'label' => 'assembly.bom_import.clear_existing_bom',
|
||||
'required' => false,
|
||||
'data' => false,
|
||||
'help' => 'assembly.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()) {
|
||||
$assembly->getBomEntries()->clear();
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
try {
|
||||
$entries = $BOMImporter->importFileIntoAssembly($form->get('file')->getData(), $assembly, [
|
||||
'type' => $form->get('type')->getData(),
|
||||
]);
|
||||
|
||||
//Validate the assembly entries
|
||||
$errors = $validator->validateProperty($assembly, 'bom_entries');
|
||||
|
||||
//If no validation errors occured, save the changes and redirect to edit page
|
||||
if (count ($errors) === 0) {
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry instanceof AssemblyBOMEntry && $entry->getPart() !== null) {
|
||||
$part = $entry->getPart();
|
||||
if ($part->getID() === null) {
|
||||
$this->partRepository->save($part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFlash('success', t('assembly.bom_import.flash.success', ['%count%' => count($entries)]));
|
||||
$entityManager->flush();
|
||||
return $this->redirectToRoute('assembly_edit', ['id' => $assembly->getID()]);
|
||||
}
|
||||
|
||||
//When we get here, there were validation errors
|
||||
$this->addFlash('error', t('assembly.bom_import.flash.invalid_entries'));
|
||||
|
||||
} catch (\UnexpectedValueException|\RuntimeException|SyntaxError $e) {
|
||||
$this->addFlash('error', t('assembly.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()]));
|
||||
}
|
||||
}
|
||||
|
||||
$jsonTemplate = [
|
||||
[
|
||||
"quantity" => 1.0,
|
||||
"name" => $this->translator->trans('assembly.bom_import.template.entry.name'),
|
||||
"part" => [
|
||||
"id" => null,
|
||||
"ipn" => $this->translator->trans('assembly.bom_import.template.entry.part.ipn'),
|
||||
"mpnr" => $this->translator->trans('assembly.bom_import.template.entry.part.mpnr'),
|
||||
"name" => $this->translator->trans('assembly.bom_import.template.entry.part.name'),
|
||||
"description" => null,
|
||||
"manufacturer" => [
|
||||
"id" => null,
|
||||
"name" => $this->translator->trans('assembly.bom_import.template.entry.part.manufacturer.name')
|
||||
],
|
||||
"category" => [
|
||||
"id" => null,
|
||||
"name" => $this->translator->trans('assembly.bom_import.template.entry.part.category.name')
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
return $this->render('assemblies/import_bom.html.twig', [
|
||||
'assembly' => $assembly,
|
||||
'jsonTemplate' => $jsonTemplate,
|
||||
'form' => $form,
|
||||
'errors' => $errors ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/add_parts', name: 'assembly_add_parts_no_id')]
|
||||
#[Route(path: '/{id}/add_parts', name: 'assembly_add_parts', requirements: ['id' => '\d+'])]
|
||||
public function addPart(Request $request, EntityManagerInterface $entityManager, ?Assembly $assembly): Response
|
||||
{
|
||||
if($assembly instanceof Assembly) {
|
||||
$this->denyAccessUnlessGranted('edit', $assembly);
|
||||
} else {
|
||||
$this->denyAccessUnlessGranted('@assemblies.edit');
|
||||
}
|
||||
|
||||
$form = $this->createForm(AssemblyAddPartsType::class, null, [
|
||||
'assembly' => $assembly,
|
||||
]);
|
||||
|
||||
//Preset the BOM entries with the selected parts, when the form was not submitted yet
|
||||
$preset_data = new ArrayCollection();
|
||||
foreach (explode(',', (string) $request->get('parts', '')) as $part_id) {
|
||||
//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;
|
||||
}
|
||||
|
||||
$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(AssemblyBOMEntry::class)->findOneBy([
|
||||
'assembly' => $assembly,
|
||||
'part' => $part
|
||||
]);
|
||||
if ($bom_entry !== null) {
|
||||
$preset_data->add($bom_entry);
|
||||
} else { //Otherwise create an empty one
|
||||
$entry = new AssemblyBOMEntry();
|
||||
$entry->setAssembly($assembly);
|
||||
$entry->setPart($part);
|
||||
$preset_data->add($entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
$form['bom_entries']->setData($preset_data);
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$target_assembly = $assembly ?? $form->get('assembly')->getData();
|
||||
|
||||
//Ensure that we really have acces to the selected assembly
|
||||
$this->denyAccessUnlessGranted('edit', $target_assembly);
|
||||
|
||||
$data = $form->getData();
|
||||
$bom_entries = $data['bom_entries'];
|
||||
foreach ($bom_entries as $bom_entry){
|
||||
$target_assembly->addBOMEntry($bom_entry);
|
||||
}
|
||||
|
||||
$entityManager->flush();
|
||||
|
||||
//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 assembly info page
|
||||
return $this->redirectToRoute('assembly_info', ['id' => $target_assembly->getID()]);
|
||||
}
|
||||
|
||||
return $this->render('assemblies/add_parts.html.twig', [
|
||||
'assembly' => $assembly,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ use App\Entity\Parts\StorageLocation;
|
|||
use App\Entity\Parts\Supplier;
|
||||
use App\Entity\PriceInformations\Orderdetail;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Exceptions\AttachmentDownloadException;
|
||||
use App\Form\Part\PartBaseType;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
|
|
@ -45,6 +46,7 @@ use App\Services\LogSystem\TimeTravel;
|
|||
use App\Services\Parameters\ParameterExtractor;
|
||||
use App\Services\Parts\PartLotWithdrawAddHelper;
|
||||
use App\Services\Parts\PricedetailHelper;
|
||||
use App\Services\AssemblySystem\AssemblyBuildPartHelper;
|
||||
use App\Services\ProjectSystem\ProjectBuildPartHelper;
|
||||
use App\Settings\BehaviorSettings\PartInfoSettings;
|
||||
use DateTime;
|
||||
|
|
@ -205,14 +207,17 @@ final class PartController extends AbstractController
|
|||
#[Route(path: '/new', name: 'part_new')]
|
||||
#[Route(path: '/{id}/clone', name: 'part_clone')]
|
||||
#[Route(path: '/new_build_part/{project_id}', name: 'part_new_build_part')]
|
||||
#[Route(path: '/new_build_part_assembly/{assembly_id}', name: 'part_new_build_part_assembly')]
|
||||
public function new(
|
||||
Request $request,
|
||||
EntityManagerInterface $em,
|
||||
TranslatorInterface $translator,
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler,
|
||||
ProjectBuildPartHelper $projectBuildPartHelper,
|
||||
AssemblyBuildPartHelper $assemblyBuildPartHelper,
|
||||
#[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null,
|
||||
#[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null
|
||||
#[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null,
|
||||
#[MapEntity(mapping: ['assembly_id' => 'id'])] ?Assembly $assembly = null
|
||||
): Response {
|
||||
|
||||
if ($part instanceof Part) {
|
||||
|
|
@ -226,6 +231,14 @@ final class PartController extends AbstractController
|
|||
return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]);
|
||||
}
|
||||
$new_part = $projectBuildPartHelper->getPartInitialization($project);
|
||||
} elseif ($assembly instanceof Assembly) {
|
||||
//Initialize a new part for a build part from the given assembly
|
||||
//Ensure that the assembly has not already a build part
|
||||
if ($project->getBuildPart() instanceof Part) {
|
||||
$this->addFlash('error', 'part.new_build_part.error.build_part_already_exists');
|
||||
return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]);
|
||||
}
|
||||
$new_part = $assemblyBuildPartHelper->getPartInitialization($assembly);
|
||||
} else { //Create an empty part from scratch
|
||||
$new_part = new Part();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\Parts\Category;
|
||||
|
|
@ -129,4 +130,17 @@ class TreeController extends AbstractController
|
|||
|
||||
return new JsonResponse($tree);
|
||||
}
|
||||
|
||||
#[Route(path: '/assembly/{id}', name: 'tree_assembly')]
|
||||
#[Route(path: '/assemblies', name: 'tree_assembly_root')]
|
||||
public function assemblyTree(?Assembly $assembly = null): JsonResponse
|
||||
{
|
||||
if ($this->isGranted('@assemblies.read')) {
|
||||
$tree = $this->treeGenerator->getTreeView(Assembly::class, $assembly, 'assemblies');
|
||||
} else {
|
||||
return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
return new JsonResponse($tree);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Services\Attachments\AssemblyPreviewGenerator;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Parts\Category;
|
||||
|
|
@ -53,6 +55,8 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* In this controller the endpoints for the typeaheads are collected.
|
||||
|
|
@ -60,8 +64,11 @@ use Symfony\Component\Serializer\Serializer;
|
|||
#[Route(path: '/typeahead')]
|
||||
class TypeaheadController extends AbstractController
|
||||
{
|
||||
public function __construct(protected AttachmentURLGenerator $urlGenerator, protected Packages $assets)
|
||||
{
|
||||
public function __construct(
|
||||
protected AttachmentURLGenerator $urlGenerator,
|
||||
protected Packages $assets,
|
||||
protected TranslatorInterface $translator
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')]
|
||||
|
|
@ -109,19 +116,22 @@ class TypeaheadController extends AbstractController
|
|||
'group' => GroupParameter::class,
|
||||
'measurement_unit' => MeasurementUnitParameter::class,
|
||||
'currency' => Currency::class,
|
||||
default => throw new \InvalidArgumentException('Invalid parameter type: '.$type),
|
||||
default => throw new InvalidArgumentException('Invalid parameter type: '.$type),
|
||||
};
|
||||
}
|
||||
|
||||
#[Route(path: '/parts/search/{query}', name: 'typeahead_parts')]
|
||||
public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator,
|
||||
AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse
|
||||
{
|
||||
public function parts(
|
||||
EntityManagerInterface $entityManager,
|
||||
PartPreviewGenerator $previewGenerator,
|
||||
AttachmentURLGenerator $attachmentURLGenerator,
|
||||
string $query = ""
|
||||
): JsonResponse {
|
||||
$this->denyAccessUnlessGranted('@parts.read');
|
||||
|
||||
$repo = $entityManager->getRepository(Part::class);
|
||||
$partRepository = $entityManager->getRepository(Part::class);
|
||||
|
||||
$parts = $repo->autocompleteSearch($query, 100);
|
||||
$parts = $partRepository->autocompleteSearch($query, 100);
|
||||
|
||||
$data = [];
|
||||
foreach ($parts as $part) {
|
||||
|
|
@ -147,6 +157,44 @@ class TypeaheadController extends AbstractController
|
|||
return new JsonResponse($data);
|
||||
}
|
||||
|
||||
#[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')]
|
||||
public function assemblies(
|
||||
EntityManagerInterface $entityManager,
|
||||
AssemblyPreviewGenerator $assemblyPreviewGenerator,
|
||||
AttachmentURLGenerator $attachmentURLGenerator,
|
||||
string $query = ""
|
||||
): JsonResponse {
|
||||
$this->denyAccessUnlessGranted('@assemblies.read');
|
||||
|
||||
$result = [];
|
||||
|
||||
$assemblyRepository = $entityManager->getRepository(Assembly::class);
|
||||
|
||||
$assemblies = $assemblyRepository->autocompleteSearch($query, 100);
|
||||
|
||||
foreach ($assemblies as $assembly) {
|
||||
$preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly);
|
||||
|
||||
if($preview_attachment instanceof Attachment) {
|
||||
$preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm');
|
||||
} else {
|
||||
$preview_url = '';
|
||||
}
|
||||
|
||||
/** @var Assembly $assembly */
|
||||
$result[] = [
|
||||
'id' => $assembly->getID(),
|
||||
'name' => $this->translator->trans('typeahead.parts.assembly.name', ['%name%' => $assembly->getName()]),
|
||||
'category' => '',
|
||||
'footprint' => '',
|
||||
'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'),
|
||||
'image' => $preview_url,
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
|
||||
#[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])]
|
||||
public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse
|
||||
{
|
||||
|
|
|
|||
209
src/DataTables/AssemblyBomEntriesDataTable.php
Normal file
209
src/DataTables/AssemblyBomEntriesDataTable.php
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
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\DataTables;
|
||||
|
||||
use App\DataTables\Column\EntityColumn;
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\MarkdownColumn;
|
||||
use App\DataTables\Helpers\PartDataTableHelper;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Services\EntityURLGenerator;
|
||||
use App\Services\Formatters\AmountFormatter;
|
||||
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;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class AssemblyBomEntriesDataTable implements DataTableTypeInterface
|
||||
{
|
||||
public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function configure(DataTable $dataTable, array $options): void
|
||||
{
|
||||
$dataTable
|
||||
//->add('select', SelectColumn::class)
|
||||
->add('picture', TextColumn::class, [
|
||||
'label' => '',
|
||||
'className' => 'no-colvis',
|
||||
'render' => function ($value, AssemblyBOMEntry $context) {
|
||||
if(!$context->getPart() instanceof Part) {
|
||||
return '';
|
||||
}
|
||||
return $this->partDataTableHelper->renderPicture($context->getPart());
|
||||
},
|
||||
])
|
||||
->add('id', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.id'),
|
||||
'visible' => false,
|
||||
])
|
||||
->add('quantity', TextColumn::class, [
|
||||
'label' => $this->translator->trans('assembly.bom.quantity'),
|
||||
'className' => 'text-center',
|
||||
'orderField' => 'bom_entry.quantity',
|
||||
'render' => function ($value, AssemblyBOMEntry $context): float|string {
|
||||
//If we have a non-part entry, only show the rounded quantity
|
||||
if (!$context->getPart() instanceof Part) {
|
||||
return round($context->getQuantity());
|
||||
}
|
||||
//Otherwise use the unit of the part to format the quantity
|
||||
return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()));
|
||||
},
|
||||
])
|
||||
->add('name', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.name'),
|
||||
'orderField' => 'NATSORT(part.name)',
|
||||
'render' => function ($value, AssemblyBOMEntry $context) {
|
||||
if(!$context->getPart() instanceof Part) {
|
||||
return htmlspecialchars((string) $context->getName());
|
||||
}
|
||||
|
||||
//Part exists if we reach this point
|
||||
|
||||
$tmp = $this->partDataTableHelper->renderName($context->getPart());
|
||||
if($context->getName() !== null && $context->getName() !== '') {
|
||||
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
|
||||
}
|
||||
return $tmp;
|
||||
},
|
||||
])
|
||||
->add('ipn', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.ipn'),
|
||||
'orderField' => 'NATSORT(part.ipn)',
|
||||
'visible' => false,
|
||||
'render' => function ($value, AssemblyBOMEntry $context) {
|
||||
if($context->getPart() instanceof Part) {
|
||||
return $context->getPart()->getIpn();
|
||||
}
|
||||
}
|
||||
])
|
||||
->add('description', MarkdownColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.description'),
|
||||
'data' => function (AssemblyBOMEntry $context) {
|
||||
if($context->getPart() instanceof Part) {
|
||||
return $context->getPart()->getDescription();
|
||||
}
|
||||
//For non-part BOM entries show the comment field
|
||||
return $context->getComment();
|
||||
},
|
||||
])
|
||||
->add('category', EntityColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.category'),
|
||||
'property' => 'part.category',
|
||||
'orderField' => 'NATSORT(category.name)',
|
||||
])
|
||||
->add('footprint', EntityColumn::class, [
|
||||
'property' => 'part.footprint',
|
||||
'label' => $this->translator->trans('part.table.footprint'),
|
||||
'orderField' => 'NATSORT(footprint.name)',
|
||||
])
|
||||
->add('manufacturer', EntityColumn::class, [
|
||||
'property' => 'part.manufacturer',
|
||||
'label' => $this->translator->trans('part.table.manufacturer'),
|
||||
'orderField' => 'NATSORT(manufacturer.name)',
|
||||
])
|
||||
->add('mountnames', TextColumn::class, [
|
||||
'label' => 'assembly.bom.mountnames',
|
||||
'render' => function ($value, AssemblyBOMEntry $context) {
|
||||
$html = '';
|
||||
|
||||
foreach (explode(',', $context->getMountnames()) as $mountname) {
|
||||
$html .= sprintf('<span class="badge badge-secondary bg-secondary">%s</span> ', htmlspecialchars($mountname));
|
||||
}
|
||||
return $html;
|
||||
},
|
||||
])
|
||||
->add('instockAmount', TextColumn::class, [
|
||||
'label' => 'assembly.bom.instockAmount',
|
||||
'visible' => false,
|
||||
'render' => function ($value, AssemblyBOMEntry $context) {
|
||||
if ($context->getPart() !== null) {
|
||||
return $this->partDataTableHelper->renderAmount($context->getPart());
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
])
|
||||
->add('storageLocations', TextColumn::class, [
|
||||
'label' => 'part.table.storeLocations',
|
||||
'visible' => false,
|
||||
'render' => function ($value, AssemblyBOMEntry $context) {
|
||||
if ($context->getPart() !== null) {
|
||||
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
])
|
||||
->add('addedDate', LocaleDateTimeColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.addedDate'),
|
||||
'visible' => false,
|
||||
])
|
||||
->add('lastModified', LocaleDateTimeColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.lastModified'),
|
||||
'visible' => false,
|
||||
])
|
||||
;
|
||||
|
||||
$dataTable->addOrderBy('name', DataTable::SORT_ASCENDING);
|
||||
|
||||
$dataTable->createAdapter(ORMAdapter::class, [
|
||||
'entity' => Attachment::class,
|
||||
'query' => function (QueryBuilder $builder) use ($options): void {
|
||||
$this->getQuery($builder, $options);
|
||||
},
|
||||
'criteria' => [
|
||||
function (QueryBuilder $builder) use ($options): void {
|
||||
$this->buildCriteria($builder, $options);
|
||||
},
|
||||
new SearchCriteriaProvider(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function getQuery(QueryBuilder $builder, array $options): void
|
||||
{
|
||||
$builder->select('bom_entry')
|
||||
->addSelect('part')
|
||||
->from(AssemblyBOMEntry::class, 'bom_entry')
|
||||
->leftJoin('bom_entry.part', 'part')
|
||||
->leftJoin('part.category', 'category')
|
||||
->leftJoin('part.footprint', 'footprint')
|
||||
->leftJoin('part.manufacturer', 'manufacturer')
|
||||
->where('bom_entry.assembly = :assembly')
|
||||
->setParameter('assembly', $options['assembly'])
|
||||
;
|
||||
}
|
||||
|
||||
private function buildCriteria(QueryBuilder $builder, array $options): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
48
src/DataTables/Helpers/AssemblyDataTableHelper.php
Normal file
48
src/DataTables/Helpers/AssemblyDataTableHelper.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
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\DataTables\Helpers;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Services\EntityURLGenerator;
|
||||
|
||||
/**
|
||||
* A helper service which contains common code to render columns for assembly related tables
|
||||
*/
|
||||
class AssemblyDataTableHelper
|
||||
{
|
||||
public function __construct(private readonly EntityURLGenerator $entityURLGenerator) {
|
||||
}
|
||||
|
||||
public function renderName(Assembly $context): string
|
||||
{
|
||||
$icon = '';
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s">%s%s</a>',
|
||||
$this->entityURLGenerator->infoURL($context),
|
||||
$icon,
|
||||
htmlspecialchars($context->getName())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,9 @@ namespace App\DataTables;
|
|||
use App\DataTables\Column\EntityColumn;
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\MarkdownColumn;
|
||||
use App\DataTables\Helpers\AssemblyDataTableHelper;
|
||||
use App\DataTables\Helpers\PartDataTableHelper;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
|
|
@ -41,11 +43,15 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||
|
||||
class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
||||
{
|
||||
public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter)
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
protected PartDataTableHelper $partDataTableHelper,
|
||||
protected AssemblyDataTableHelper $assemblyDataTableHelper,
|
||||
protected EntityURLGenerator $entityURLGenerator,
|
||||
protected AmountFormatter $amountFormatter
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public function configure(DataTable $dataTable, array $options): void
|
||||
{
|
||||
$dataTable
|
||||
|
|
@ -84,16 +90,26 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
'label' => $this->translator->trans('part.table.name'),
|
||||
'orderField' => 'NATSORT(part.name)',
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
if(!$context->getPart() instanceof Part) {
|
||||
if(!$context->getPart() instanceof Part && !$context->getAssembly() instanceof Assembly) {
|
||||
return htmlspecialchars((string) $context->getName());
|
||||
}
|
||||
|
||||
//Part exists if we reach this point
|
||||
if ($context->getPart() !== null) {
|
||||
$tmp = $this->partDataTableHelper->renderName($context->getPart());
|
||||
$tmp = $this->translator->trans('part.table.name.value.for_part', ['%value%' => $tmp]);
|
||||
|
||||
$tmp = $this->partDataTableHelper->renderName($context->getPart());
|
||||
if($context->getName() !== null && $context->getName() !== '') {
|
||||
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
|
||||
if($context->getName() !== null && $context->getName() !== '') {
|
||||
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
|
||||
}
|
||||
} elseif ($context->getAssembly() !== null) {
|
||||
$tmp = $this->assemblyDataTableHelper->renderName($context->getAssembly());
|
||||
$tmp = $this->translator->trans('part.table.name.value.for_assembly', ['%value%' => $tmp]);
|
||||
|
||||
if($context->getName() !== null && $context->getName() !== '') {
|
||||
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
|
||||
}
|
||||
}
|
||||
|
||||
return $tmp;
|
||||
},
|
||||
])
|
||||
|
|
|
|||
358
src/Entity/AssemblySystem/Assembly.php
Normal file
358
src/Entity/AssemblySystem/Assembly.php
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
<?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\Entity\AssemblySystem;
|
||||
|
||||
use App\Repository\AssemblyRepository;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Link;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\OpenApi\Model\Operation;
|
||||
use ApiPlatform\Serializer\Filter\PropertyFilter;
|
||||
use App\ApiPlatform\Filter\LikeFilter;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Validator\Constraints\UniqueObjectCollection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use App\Entity\Attachments\AssemblyAttachment;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\Parameters\AssemblyParameter;
|
||||
use App\Entity\Parts\Part;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
/**
|
||||
* This class represents a assembly in the database.
|
||||
*
|
||||
* @extends AbstractStructuralDBElement<AssemblyAttachment, AssemblyParameter>
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: AssemblyRepository::class)]
|
||||
#[ORM\Table(name: 'assemblies')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: 'is_granted("read", object)'),
|
||||
new GetCollection(security: 'is_granted("@assemblies.read")'),
|
||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||
new Patch(security: 'is_granted("edit", object)'),
|
||||
new Delete(security: 'is_granted("delete", object)'),
|
||||
],
|
||||
normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||
denormalizationContext: ['groups' => ['assembly:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||
)]
|
||||
#[ApiResource(
|
||||
uriTemplate: '/assemblies/{id}/children.{_format}',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
openapi: new Operation(summary: 'Retrieves the children elements of a assembly.'),
|
||||
security: 'is_granted("@assemblies.read")'
|
||||
)
|
||||
],
|
||||
uriVariables: [
|
||||
'id' => new Link(fromProperty: 'children', fromClass: Assembly::class)
|
||||
],
|
||||
normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
||||
)]
|
||||
#[ApiFilter(PropertyFilter::class)]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
|
||||
class Assembly extends AbstractStructuralDBElement
|
||||
{
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
protected Collection $children;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||
#[ORM\JoinColumn(name: 'parent_id')]
|
||||
#[Groups(['assembly:read', 'assembly:write'])]
|
||||
#[ApiProperty(readableLink: false, writableLink: false)]
|
||||
protected ?AbstractStructuralDBElement $parent = null;
|
||||
|
||||
#[Groups(['assembly:read', 'assembly:write'])]
|
||||
protected string $comment = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, AssemblyBOMEntry>
|
||||
*/
|
||||
#[Assert\Valid]
|
||||
#[Groups(['extended', 'full', 'import'])]
|
||||
#[ORM\OneToMany(mappedBy: 'assembly', targetEntity: AssemblyBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])]
|
||||
#[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])]
|
||||
protected Collection $bom_entries;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
protected int $order_quantity = 0;
|
||||
|
||||
/**
|
||||
* @var string|null The current status of the assembly
|
||||
*/
|
||||
#[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])]
|
||||
#[Groups(['extended', 'full', 'assembly:read', 'assembly:write', 'import'])]
|
||||
#[ORM\Column(type: Types::STRING, length: 64, nullable: true)]
|
||||
protected ?string $status = null;
|
||||
|
||||
|
||||
/**
|
||||
* @var Part|null The (optional) part that represents the builds of this assembly in the stock
|
||||
*/
|
||||
#[ORM\OneToOne(mappedBy: 'built_assembly', targetEntity: Part::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['assembly:read', 'assembly:write'])]
|
||||
protected ?Part $build_part = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN)]
|
||||
protected bool $order_only_missing_parts = false;
|
||||
|
||||
#[Groups(['simple', 'extended', 'full', 'assembly:read', 'assembly:write'])]
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
protected string $description = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, AssemblyAttachment>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: AssemblyAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
#[Groups(['assembly:read', 'assembly:write'])]
|
||||
protected Collection $attachments;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: AssemblyAttachment::class)]
|
||||
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
|
||||
#[Groups(['assembly:read', 'assembly:write'])]
|
||||
protected ?Attachment $master_picture_attachment = null;
|
||||
|
||||
/** @var Collection<int, AssemblyParameter>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: AssemblyParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
|
||||
#[Groups(['assembly:read', 'assembly:write'])]
|
||||
protected Collection $parameters;
|
||||
|
||||
#[Groups(['assembly:read'])]
|
||||
protected ?\DateTimeImmutable $addedDate = null;
|
||||
#[Groups(['assembly:read'])]
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
*
|
||||
* Getters
|
||||
*
|
||||
*********************************************************************************/
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->attachments = new ArrayCollection();
|
||||
$this->parameters = new ArrayCollection();
|
||||
parent::__construct();
|
||||
$this->bom_entries = new ArrayCollection();
|
||||
$this->children = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
//When cloning this assembly, we have to clone each bom entry too.
|
||||
if ($this->id) {
|
||||
$bom_entries = $this->bom_entries;
|
||||
$this->bom_entries = new ArrayCollection();
|
||||
//Set master attachment is needed
|
||||
foreach ($bom_entries as $bom_entry) {
|
||||
$clone = clone $bom_entry;
|
||||
$this->addBomEntry($clone);
|
||||
}
|
||||
}
|
||||
|
||||
//Parent has to be last call, as it resets the ID
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the order quantity of this assembly.
|
||||
*
|
||||
* @return int the order quantity
|
||||
*/
|
||||
public function getOrderQuantity(): int
|
||||
{
|
||||
return $this->order_quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "order_only_missing_parts" attribute.
|
||||
*
|
||||
* @return bool the "order_only_missing_parts" attribute
|
||||
*/
|
||||
public function getOrderOnlyMissingParts(): bool
|
||||
{
|
||||
return $this->order_only_missing_parts;
|
||||
}
|
||||
|
||||
/********************************************************************************
|
||||
*
|
||||
* Setters
|
||||
*
|
||||
*********************************************************************************/
|
||||
|
||||
/**
|
||||
* Set the order quantity.
|
||||
*
|
||||
* @param int $new_order_quantity the new order quantity
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setOrderQuantity(int $new_order_quantity): self
|
||||
{
|
||||
if ($new_order_quantity < 0) {
|
||||
throw new InvalidArgumentException('The new order quantity must not be negative!');
|
||||
}
|
||||
$this->order_quantity = $new_order_quantity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the "order_only_missing_parts" attribute.
|
||||
*
|
||||
* @param bool $new_order_only_missing_parts the new "order_only_missing_parts" attribute
|
||||
*/
|
||||
public function setOrderOnlyMissingParts(bool $new_order_only_missing_parts): self
|
||||
{
|
||||
$this->order_only_missing_parts = $new_order_only_missing_parts;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBomEntries(): Collection
|
||||
{
|
||||
return $this->bom_entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function addBomEntry(AssemblyBOMEntry $entry): self
|
||||
{
|
||||
$entry->setAssembly($this);
|
||||
$this->bom_entries->add($entry);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function removeBomEntry(AssemblyBOMEntry $entry): self
|
||||
{
|
||||
$this->bom_entries->removeElement($entry);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): Assembly
|
||||
{
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStatus(): ?string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $status
|
||||
*/
|
||||
public function setStatus(?string $status): void
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this assembly has an associated part representing the builds of this assembly in the stock.
|
||||
*/
|
||||
public function hasBuildPart(): bool
|
||||
{
|
||||
return $this->build_part instanceof Part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the part representing the builds of this assembly in the stock, if it is existing
|
||||
*/
|
||||
public function getBuildPart(): ?Part
|
||||
{
|
||||
return $this->build_part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the part representing the builds of this assembly in the stock.
|
||||
*/
|
||||
public function setBuildPart(?Part $build_part): void
|
||||
{
|
||||
$this->build_part = $build_part;
|
||||
if ($build_part instanceof Part) {
|
||||
$build_part->setBuiltAssembly($this);
|
||||
}
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validate(ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
//If this assembly has subassemblies, and these have builds part, they must be included in the BOM
|
||||
foreach ($this->getChildren() as $child) {
|
||||
if (!$child->getBuildPart() instanceof Part) {
|
||||
continue;
|
||||
}
|
||||
//We have to search all bom entries for the build part
|
||||
$found = false;
|
||||
foreach ($this->getBomEntries() as $bom_entry) {
|
||||
if ($bom_entry->getPart() === $child->getBuildPart()) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//When the build part is not found, we have to add an error
|
||||
if (!$found) {
|
||||
$context->buildViolation('assembly.bom_has_to_include_all_subelement_parts')
|
||||
->atPath('bom_entries')
|
||||
->setParameter('%assembly_name%', $child->getName())
|
||||
->setParameter('%part_name%', $child->getBuildPart()->getName())
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
302
src/Entity/AssemblySystem/AssemblyBOMEntry.php
Normal file
302
src/Entity/AssemblySystem/AssemblyBOMEntry.php
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
<?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\Entity\AssemblySystem;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Link;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\OpenApi\Model\Operation;
|
||||
use ApiPlatform\Serializer\Filter\PropertyFilter;
|
||||
use App\ApiPlatform\Filter\LikeFilter;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Repository\DBElementRepository;
|
||||
use App\Validator\UniqueValidatableInterface;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\TimestampTrait;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Validator\Constraints\BigDecimal\BigDecimalPositive;
|
||||
use App\Validator\Constraints\Selectable;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
/**
|
||||
* The AssemblyBOMEntry class represents an entry in a assembly's BOM.
|
||||
*/
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\Entity(repositoryClass: DBElementRepository::class)]
|
||||
#[ORM\Table('assembly_bom_entries')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("read", object)',),
|
||||
new GetCollection(uriTemplate: '/assembly_bom_entries.{_format}', security: 'is_granted("@assemblies.read")',),
|
||||
new Post(uriTemplate: '/assembly_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',),
|
||||
new Patch(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',),
|
||||
new Delete(uriTemplate: '/assembly_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',),
|
||||
],
|
||||
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||
denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
||||
)]
|
||||
#[ApiResource(
|
||||
uriTemplate: '/assemblies/{id}/bom.{_format}',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
openapi: new Operation(summary: 'Retrieves the BOM entries of the given assembly.'),
|
||||
security: 'is_granted("@assemblies.read")'
|
||||
)
|
||||
],
|
||||
uriVariables: [
|
||||
'id' => new Link(fromProperty: 'bom_entries', fromClass: Assembly::class)
|
||||
],
|
||||
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
||||
)]
|
||||
#[ApiFilter(PropertyFilter::class)]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
|
||||
#[ApiFilter(RangeFilter::class, properties: ['quantity'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified', 'quantity'])]
|
||||
class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInterface, TimeStampableInterface
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
#[Assert\Positive]
|
||||
#[ORM\Column(name: 'quantity', type: Types::FLOAT)]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
|
||||
protected float $quantity = 1.0;
|
||||
|
||||
/**
|
||||
* @var string A comma separated list of the names, where this parts should be placed
|
||||
*/
|
||||
#[ORM\Column(name: 'mountnames', type: Types::TEXT)]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
|
||||
protected string $mountnames = '';
|
||||
|
||||
/**
|
||||
* @var string|null An optional name describing this BOM entry (useful for non-part entries)
|
||||
*/
|
||||
#[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.assembly.bom_entry.name_or_part_needed')]
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
|
||||
protected ?string $name = null;
|
||||
|
||||
/**
|
||||
* @var string An optional comment for this BOM entry
|
||||
*/
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])]
|
||||
protected string $comment = '';
|
||||
|
||||
/**
|
||||
* @var Assembly|null
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'bom_entries')]
|
||||
#[ORM\JoinColumn(name: 'id_assembly', nullable: true)]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', ])]
|
||||
protected ?Assembly $assembly = null;
|
||||
|
||||
/**
|
||||
* @var Part|null The part associated with this
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'assembly_bom_entries')]
|
||||
#[ORM\JoinColumn(name: 'id_part')]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', 'full'])]
|
||||
protected ?Part $part = null;
|
||||
|
||||
/**
|
||||
* @var BigDecimal|null The price of this non-part BOM entry
|
||||
*/
|
||||
#[Assert\AtLeastOneOf([new BigDecimalPositive(), new Assert\IsNull()])]
|
||||
#[ORM\Column(type: 'big_decimal', precision: 11, scale: 5, nullable: true)]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])]
|
||||
protected ?BigDecimal $price = null;
|
||||
|
||||
/**
|
||||
* @var ?Currency The currency for the price of this non-part BOM entry
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: Currency::class)]
|
||||
#[ORM\JoinColumn]
|
||||
#[Selectable]
|
||||
protected ?Currency $price_currency = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function getQuantity(): float
|
||||
{
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function setQuantity(float $quantity): AssemblyBOMEntry
|
||||
{
|
||||
$this->quantity = $quantity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMountnames(): string
|
||||
{
|
||||
return $this->mountnames;
|
||||
}
|
||||
|
||||
public function setMountnames(string $mountnames): AssemblyBOMEntry
|
||||
{
|
||||
$this->mountnames = $mountnames;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*/
|
||||
public function setName(?string $name): AssemblyBOMEntry
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function setComment(string $comment): AssemblyBOMEntry
|
||||
{
|
||||
$this->comment = $comment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAssembly(): ?Assembly
|
||||
{
|
||||
return $this->assembly;
|
||||
}
|
||||
|
||||
public function setAssembly(?Assembly $assembly): AssemblyBOMEntry
|
||||
{
|
||||
$this->assembly = $assembly;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPart(): ?Part
|
||||
{
|
||||
return $this->part;
|
||||
}
|
||||
|
||||
public function setPart(?Part $part): AssemblyBOMEntry
|
||||
{
|
||||
$this->part = $part;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the price of this BOM entry, if existing.
|
||||
* Prices are only valid on non-Part BOM entries.
|
||||
*/
|
||||
public function getPrice(): ?BigDecimal
|
||||
{
|
||||
return $this->price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the price of this BOM entry.
|
||||
* Prices are only valid on non-Part BOM entries.
|
||||
*/
|
||||
public function setPrice(?BigDecimal $price): void
|
||||
{
|
||||
$this->price = $price;
|
||||
}
|
||||
|
||||
public function getPriceCurrency(): ?Currency
|
||||
{
|
||||
return $this->price_currency;
|
||||
}
|
||||
|
||||
public function setPriceCurrency(?Currency $price_currency): void
|
||||
{
|
||||
$this->price_currency = $price_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this BOM entry is a part associated BOM entry or not.
|
||||
* @return bool True if this BOM entry is a part associated BOM entry, false otherwise.
|
||||
*/
|
||||
public function isPartBomEntry(): bool
|
||||
{
|
||||
return $this->part instanceof Part;
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validate(ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
//Round quantity to whole numbers, if the part is not a decimal part
|
||||
if ($this->part instanceof Part && (!$this->part->getPartUnit() || $this->part->getPartUnit()->isInteger())) {
|
||||
$this->quantity = round($this->quantity);
|
||||
}
|
||||
//Non-Part BOM entries are rounded
|
||||
if (!$this->part instanceof Part) {
|
||||
$this->quantity = round($this->quantity);
|
||||
}
|
||||
|
||||
//Check that the part is not the build representation part of this assembly or one of its parents
|
||||
if ($this->part && $this->part->getBuiltAssembly() instanceof Assembly) {
|
||||
//Get the associated assembly
|
||||
$associated_assembly = $this->part->getBuiltAssembly();
|
||||
//Check that it is not the same as the current assembly neither one of its parents
|
||||
$current_assembly = $this->assembly;
|
||||
while ($current_assembly) {
|
||||
if ($associated_assembly === $current_assembly) {
|
||||
$context->buildViolation('assembly.bom_entry.can_not_add_own_builds_part')
|
||||
->atPath('part')
|
||||
->addViolation();
|
||||
}
|
||||
$current_assembly = $current_assembly->getParent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getComparableFields(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->getName(),
|
||||
'part' => $this->getPart()?->getID(),
|
||||
];
|
||||
}
|
||||
}
|
||||
48
src/Entity/Attachments/AssemblyAttachment.php
Normal file
48
src/Entity/Attachments/AssemblyAttachment.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?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\Entity\Attachments;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Serializer\APIPlatform\OverrideClassDenormalizer;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
|
||||
/**
|
||||
* A attachment attached to a device element.
|
||||
* @extends Attachment<Assembly>
|
||||
*/
|
||||
#[UniqueEntity(['name', 'attachment_type', 'element'])]
|
||||
#[UniqueEntity(['name', 'attachment_type', 'element'])]
|
||||
#[ORM\Entity]
|
||||
class AssemblyAttachment extends Attachment
|
||||
{
|
||||
final public const ALLOWED_ELEMENT_CLASS = Assembly::class;
|
||||
/**
|
||||
* @var Assembly|null the element this attachment is associated with
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'attachments')]
|
||||
#[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')]
|
||||
#[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])]
|
||||
protected ?AttachmentContainingDBElement $element = null;
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ use function in_array;
|
|||
#[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)]
|
||||
abstract class Attachment extends AbstractNamedDBElement
|
||||
{
|
||||
private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class,
|
||||
private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, 'Assembly' => AssemblyAttachment::class,
|
||||
'AttachmentType' => AttachmentTypeAttachment::class,
|
||||
'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class,
|
||||
'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class,
|
||||
|
|
@ -107,7 +107,8 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
/*
|
||||
* The discriminator map used for API platform. The key should be the same as the api platform short type (the @type JSONLD field).
|
||||
*/
|
||||
private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "AttachmentType" => AttachmentTypeAttachment::class,
|
||||
private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "Assembly" => AssemblyAttachment::class,
|
||||
"AttachmentType" => AttachmentTypeAttachment::class,
|
||||
"Category" => CategoryAttachment::class, "Footprint" => FootprintAttachment::class, "Manufacturer" => ManufacturerAttachment::class,
|
||||
"Currency" => CurrencyAttachment::class, "Group" => GroupAttachment::class, "MeasurementUnit" => MeasurementUnitAttachment::class,
|
||||
"StorageLocation" => StorageLocationAttachment::class, "Supplier" => SupplierAttachment::class, "User" => UserAttachment::class, "LabelProfile" => LabelAttachment::class];
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\Base;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Attachments\AssemblyAttachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentTypeAttachment;
|
||||
|
|
@ -68,7 +71,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||
* Every database table which are managed with this class (or a subclass of it)
|
||||
* must have the table row "id"!! The ID is the unique key to identify the elements.
|
||||
*/
|
||||
#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])]
|
||||
#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'assembly_attachment' => AssemblyAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'assembly' => Assembly::class, 'assembly_bom_entry' => AssemblyBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => Pricedetail::class, 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])]
|
||||
#[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)]
|
||||
abstract class AbstractDBElement implements JsonSerializable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Attachments\AssemblyAttachment;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\AttachmentTypeAttachment;
|
||||
|
|
@ -58,6 +60,7 @@ 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\ProjectSystem\Project;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Entity\Parameters\AttachmentTypeParameter;
|
||||
|
|
@ -147,6 +150,7 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU
|
|||
{
|
||||
if (is_a($abstract_class, AbstractParameter::class, true)) {
|
||||
return match ($this->getTargetClass()) {
|
||||
Assembly::class => AssemblyParameter::class,
|
||||
AttachmentType::class => AttachmentTypeParameter::class,
|
||||
Category::class => CategoryParameter::class,
|
||||
Currency::class => CurrencyParameter::class,
|
||||
|
|
@ -168,6 +172,7 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU
|
|||
Category::class => CategoryAttachment::class,
|
||||
Currency::class => CurrencyAttachment::class,
|
||||
Project::class => ProjectAttachment::class,
|
||||
Assembly::class => AssemblyAttachment::class,
|
||||
Footprint::class => FootprintAttachment::class,
|
||||
Group::class => GroupAttachment::class,
|
||||
Manufacturer::class => ManufacturerAttachment::class,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ use App\Entity\PriceInformations\Orderdetail;
|
|||
use App\Entity\PriceInformations\Pricedetail;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\User;
|
||||
|
||||
|
|
@ -71,6 +73,8 @@ enum LogTargetType: int
|
|||
case PART_ASSOCIATION = 20;
|
||||
case BULK_INFO_PROVIDER_IMPORT_JOB = 21;
|
||||
case BULK_INFO_PROVIDER_IMPORT_JOB_PART = 22;
|
||||
case ASSEMBLY = 23;
|
||||
case ASSEMBLY_BOM_ENTRY = 24;
|
||||
|
||||
/**
|
||||
* Returns the class name of the target type or null if the target type is NONE.
|
||||
|
|
@ -86,6 +90,8 @@ enum LogTargetType: int
|
|||
self::CATEGORY => Category::class,
|
||||
self::PROJECT => Project::class,
|
||||
self::BOM_ENTRY => ProjectBOMEntry::class,
|
||||
self::ASSEMBLY => Assembly::class,
|
||||
self::ASSEMBLY_BOM_ENTRY => AssemblyBOMEntry::class,
|
||||
self::FOOTPRINT => Footprint::class,
|
||||
self::GROUP => Group::class,
|
||||
self::MANUFACTURER => Manufacturer::class,
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ use function sprintf;
|
|||
#[ORM\DiscriminatorMap([0 => CategoryParameter::class, 1 => CurrencyParameter::class, 2 => ProjectParameter::class,
|
||||
3 => FootprintParameter::class, 4 => GroupParameter::class, 5 => ManufacturerParameter::class,
|
||||
6 => MeasurementUnitParameter::class, 7 => PartParameter::class, 8 => StorageLocationParameter::class,
|
||||
9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class])]
|
||||
9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class, 11 => AssemblyParameter::class])]
|
||||
#[ORM\Table('parameters')]
|
||||
#[ORM\Index(columns: ['name'], name: 'parameter_name_idx')]
|
||||
#[ORM\Index(columns: ['param_group'], name: 'parameter_group_idx')]
|
||||
|
|
@ -103,7 +103,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
*/
|
||||
private const API_DISCRIMINATOR_MAP = ["Part" => PartParameter::class,
|
||||
"AttachmentType" => AttachmentTypeParameter::class, "Category" => CategoryParameter::class, "Currency" => CurrencyParameter::class,
|
||||
"Project" => ProjectParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class,
|
||||
"Project" => ProjectParameter::class, "Assembly" => AssemblyParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class,
|
||||
"Manufacturer" => ManufacturerParameter::class, "MeasurementUnit" => MeasurementUnitParameter::class,
|
||||
"StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class];
|
||||
|
||||
|
|
|
|||
65
src/Entity/Parameters/AssemblyParameter.php
Normal file
65
src/Entity/Parameters/AssemblyParameter.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?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\Entity\Parameters;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Repository\ParameterRepository;
|
||||
use App\Serializer\APIPlatform\OverrideClassDenormalizer;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
|
||||
#[UniqueEntity(fields: ['name', 'group', 'element'])]
|
||||
#[ORM\Entity(repositoryClass: ParameterRepository::class)]
|
||||
class AssemblyParameter extends AbstractParameter
|
||||
{
|
||||
final public const ALLOWED_ELEMENT_CLASS = Assembly::class;
|
||||
|
||||
/**
|
||||
* @var Assembly the element this para is associated with
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'parameters')]
|
||||
#[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')]
|
||||
#[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])]
|
||||
protected ?AbstractDBElement $element = null;
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ use App\Entity\Parts\PartTraits\InstockTrait;
|
|||
use App\Entity\Parts\PartTraits\ManufacturerTrait;
|
||||
use App\Entity\Parts\PartTraits\OrderTrait;
|
||||
use App\Entity\Parts\PartTraits\ProjectTrait;
|
||||
use App\Entity\Parts\PartTraits\AssemblyTrait;
|
||||
use App\EntityListeners\TreeCacheInvalidationListener;
|
||||
use App\Repository\PartRepository;
|
||||
use App\Validator\Constraints\UniqueObjectCollection;
|
||||
|
|
@ -125,6 +126,7 @@ class Part extends AttachmentContainingDBElement
|
|||
use OrderTrait;
|
||||
use ParametersTrait;
|
||||
use ProjectTrait;
|
||||
use AssemblyTrait;
|
||||
use AssociationTrait;
|
||||
use EDATrait;
|
||||
|
||||
|
|
@ -186,6 +188,7 @@ class Part extends AttachmentContainingDBElement
|
|||
$this->orderdetails = new ArrayCollection();
|
||||
$this->parameters = new ArrayCollection();
|
||||
$this->project_bom_entries = new ArrayCollection();
|
||||
$this->assembly_bom_entries = new ArrayCollection();
|
||||
|
||||
$this->associated_parts_as_owner = new ArrayCollection();
|
||||
$this->associated_parts_as_other = new ArrayCollection();
|
||||
|
|
|
|||
83
src/Entity/Parts/PartTraits/AssemblyTrait.php
Normal file
83
src/Entity/Parts/PartTraits/AssemblyTrait.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\Parts\PartTraits;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
trait AssemblyTrait
|
||||
{
|
||||
/**
|
||||
* @var Collection<AssemblyBOMEntry> $assembly_bom_entries
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'part', targetEntity: AssemblyBOMEntry::class, cascade: ['remove'], orphanRemoval: true)]
|
||||
protected Collection $assembly_bom_entries;
|
||||
|
||||
/**
|
||||
* @var Assembly|null If a assembly is set here, then this part is special and represents the builds of an assembly.
|
||||
*/
|
||||
#[ORM\OneToOne(inversedBy: 'build_part', targetEntity: Assembly::class)]
|
||||
#[ORM\JoinColumn]
|
||||
protected ?Assembly $built_assembly = null;
|
||||
|
||||
/**
|
||||
* Returns all AssemblyBOMEntry that use this part.
|
||||
*
|
||||
* @phpstan-return Collection<int, AssemblyBOMEntry>
|
||||
*/
|
||||
public function getAssemblyBomEntries(): Collection
|
||||
{
|
||||
return $this->assembly_bom_entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this part represents the builds of a assembly
|
||||
* @return bool True if it represents the builds, false if not
|
||||
*/
|
||||
#[Groups(['part:read'])]
|
||||
public function isAssemblyBuildPart(): bool
|
||||
{
|
||||
return $this->built_assembly !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the assembly that this part represents the builds of, or null if it doesn't
|
||||
*/
|
||||
public function getBuiltAssembly(): ?Assembly
|
||||
{
|
||||
return $this->built_assembly;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the assembly that this part represents the builds of
|
||||
* @param Assembly|null $built_assembly The assembly that this part represents the builds of, or null if it is not a build part
|
||||
*/
|
||||
public function setBuiltAssembly(?Assembly $built_assembly): self
|
||||
{
|
||||
$this->built_assembly = $built_assembly;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all assemblies which uses this part.
|
||||
*
|
||||
* @return Assembly[] all assemblies which uses this part as a one-dimensional array of Assembly objects
|
||||
*/
|
||||
public function getAssemblies(): array
|
||||
{
|
||||
$assemblies = [];
|
||||
|
||||
foreach($this->assembly_bom_entries as $entry) {
|
||||
$assemblies[] = $entry->getAssembly();
|
||||
}
|
||||
|
||||
return $assemblies;
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +108,7 @@ class Project extends AbstractStructuralDBElement
|
|||
#[Groups(['extended', 'full', 'import'])]
|
||||
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part'])]
|
||||
#[UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly'])]
|
||||
#[UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name'])]
|
||||
protected Collection $bom_entries;
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ use ApiPlatform\Metadata\Post;
|
|||
use ApiPlatform\OpenApi\Model\Operation;
|
||||
use ApiPlatform\Serializer\Filter\PropertyFilter;
|
||||
use App\ApiPlatform\Filter\LikeFilter;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use App\Validator\UniqueValidatableInterface;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
|
@ -103,7 +104,10 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
|
|||
/**
|
||||
* @var string|null An optional name describing this BOM entry (useful for non-part entries)
|
||||
*/
|
||||
#[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.project.bom_entry.name_or_part_needed')]
|
||||
#[Assert\Expression(
|
||||
'this.getPart() !== null or this.getAssembly() !== null or (this.getName() !== null and this.getName() != "")',
|
||||
message: 'validator.project.bom_entry.part_or_assembly_needed'
|
||||
)]
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
|
||||
protected ?string $name = null;
|
||||
|
|
@ -131,6 +135,18 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
|
|||
#[Groups(['bom_entry:read', 'bom_entry:write', 'full'])]
|
||||
protected ?Part $part = null;
|
||||
|
||||
/**
|
||||
* @var Assembly|null The associated assembly
|
||||
*/
|
||||
#[Assert\Expression(
|
||||
'(this.getPart() === null or this.getAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))',
|
||||
message: 'validator.project.bom_entry.only_part_or_assembly_allowed'
|
||||
)]
|
||||
#[ORM\ManyToOne(targetEntity: Assembly::class, inversedBy: 'assembly_bom_entries')]
|
||||
#[ORM\JoinColumn(name: 'id_assembly')]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', ])]
|
||||
protected ?Assembly $assembly = null;
|
||||
|
||||
/**
|
||||
* @var BigDecimal|null The price of this non-part BOM entry
|
||||
*/
|
||||
|
|
@ -212,8 +228,6 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
|
|||
return $this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function getPart(): ?Part
|
||||
{
|
||||
return $this->part;
|
||||
|
|
@ -225,6 +239,16 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getAssembly(): ?Assembly
|
||||
{
|
||||
return $this->assembly;
|
||||
}
|
||||
|
||||
public function setAssembly(?Assembly $assembly): void
|
||||
{
|
||||
$this->assembly = $assembly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the price of this BOM entry, if existing.
|
||||
* Prices are only valid on non-Part BOM entries.
|
||||
|
|
@ -262,6 +286,15 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
|
|||
return $this->part instanceof Part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this BOM entry is a assembly associated BOM entry or not.
|
||||
* @return bool True if this BOM entry is a assembly associated BOM entry, false otherwise.
|
||||
*/
|
||||
public function isAssemblyBomEntry(): bool
|
||||
{
|
||||
return $this->assembly instanceof Assembly;
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validate(ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
|
|
@ -323,6 +356,7 @@ class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInte
|
|||
return [
|
||||
'name' => $this->getName(),
|
||||
'part' => $this->getPart()?->getID(),
|
||||
'assembly' => $this->getAssembly()?->getID(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
src/Form/AdminPages/AssemblyAdminForm.php
Normal file
64
src/Form/AdminPages/AssemblyAdminForm.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
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\Form\AdminPages;
|
||||
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType;
|
||||
use App\Form\Type\RichTextEditorType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class AssemblyAdminForm extends BaseEntityAdminForm
|
||||
{
|
||||
protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void
|
||||
{
|
||||
$builder->add('description', RichTextEditorType::class, [
|
||||
'required' => false,
|
||||
'label' => 'part.edit.description',
|
||||
'mode' => 'markdown-single_line',
|
||||
'empty_data' => '',
|
||||
'attr' => [
|
||||
'placeholder' => 'part.edit.description.placeholder',
|
||||
'rows' => 2,
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->add('bom_entries', AssemblyBOMEntryCollectionType::class);
|
||||
|
||||
$builder->add('status', ChoiceType::class, [
|
||||
'attr' => [
|
||||
'class' => 'form-select',
|
||||
],
|
||||
'label' => 'assembly.edit.status',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'choices' => [
|
||||
'assembly.status.draft' => 'draft',
|
||||
'assembly.status.planning' => 'planning',
|
||||
'assembly.status.in_production' => 'in_production',
|
||||
'assembly.status.finished' => 'finished',
|
||||
'assembly.status.archived' => 'archived',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Form\AdminPages;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\UserSystem\Group;
|
||||
|
|
@ -114,7 +115,7 @@ class BaseEntityAdminForm extends AbstractType
|
|||
);
|
||||
}
|
||||
|
||||
if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Currency)) {
|
||||
if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Assembly || $entity instanceof Currency)) {
|
||||
$builder->add('alternative_names', TextType::class, [
|
||||
'required' => false,
|
||||
'label' => 'entity.edit.alternative_names.label',
|
||||
|
|
|
|||
88
src/Form/AssemblySystem/AssemblyAddPartsType.php
Normal file
88
src/Form/AssemblySystem/AssemblyAddPartsType.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\Form\AssemblySystem;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Form\Type\StructuralEntityType;
|
||||
use App\Validator\Constraints\UniqueObjectCollection;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
|
||||
class AssemblyAddPartsType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('assembly', StructuralEntityType::class, [
|
||||
'class' => Assembly::class,
|
||||
'required' => true,
|
||||
'disabled' => $options['assembly'] instanceof Assembly, //If a assembly is given, disable the field
|
||||
'data' => $options['assembly'],
|
||||
'constraints' => [
|
||||
new NotNull()
|
||||
]
|
||||
]);
|
||||
$builder->add('bom_entries', AssemblyBOMEntryCollectionType::class, [
|
||||
'entry_options' => [
|
||||
'constraints' => [
|
||||
new UniqueEntity(fields: ['part', 'assembly'], message: 'assembly.bom_entry.part_already_in_bom',
|
||||
entityClass: AssemblyBOMEntry::class),
|
||||
new UniqueEntity(fields: ['name', 'assembly'], message: 'assembly.bom_entry.name_already_in_bom',
|
||||
entityClass: AssemblyBOMEntry::class, ignoreNull: true),
|
||||
]
|
||||
],
|
||||
'constraints' => [
|
||||
new UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part']),
|
||||
new UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name']),
|
||||
]
|
||||
]);
|
||||
$builder->add('submit', SubmitType::class, ['label' => 'save']);
|
||||
|
||||
//After submit set the assembly for all bom entries, so that it can be validated properly
|
||||
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
|
||||
$form = $event->getForm();
|
||||
/** @var Assembly $assembly */
|
||||
$assembly = $form->get('assembly')->getData();
|
||||
$bom_entries = $form->get('bom_entries')->getData();
|
||||
|
||||
foreach ($bom_entries as $bom_entry) {
|
||||
$bom_entry->setAssembly($assembly);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'assembly' => null,
|
||||
]);
|
||||
|
||||
$resolver->setAllowedTypes('assembly', ['null', Assembly::class]);
|
||||
}
|
||||
}
|
||||
32
src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php
Normal file
32
src/Form/AssemblySystem/AssemblyBOMEntryCollectionType.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\AssemblySystem;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AssemblyBOMEntryCollectionType extends AbstractType
|
||||
{
|
||||
public function getParent(): string
|
||||
{
|
||||
return CollectionType::class;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'entry_type' => AssemblyBOMEntryType::class,
|
||||
'entry_options' => [
|
||||
'label' => false,
|
||||
],
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
'by_reference' => false,
|
||||
'reindex_enable' => true,
|
||||
'label' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
90
src/Form/AssemblySystem/AssemblyBOMEntryType.php
Normal file
90
src/Form/AssemblySystem/AssemblyBOMEntryType.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\AssemblySystem;
|
||||
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Form\Type\BigDecimalNumberType;
|
||||
use App\Form\Type\CurrencyEntityType;
|
||||
use App\Form\Type\PartSelectType;
|
||||
use App\Form\Type\RichTextEditorType;
|
||||
use App\Form\Type\SIUnitType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Event\PreSetDataEvent;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AssemblyBOMEntryType extends AbstractType
|
||||
{
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
|
||||
$form = $event->getForm();
|
||||
/** @var AssemblyBOMEntry $data */
|
||||
$data = $event->getData();
|
||||
|
||||
$form->add('quantity', SIUnitType::class, [
|
||||
'label' => 'assembly.bom.quantity',
|
||||
'measurement_unit' => $data && $data->getPart() ? $data->getPart()->getPartUnit() : null,
|
||||
]);
|
||||
});
|
||||
|
||||
$builder
|
||||
|
||||
->add('part', PartSelectType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'assembly.bom.name',
|
||||
'required' => false,
|
||||
])
|
||||
->add('mountnames', TextType::class, [
|
||||
'required' => false,
|
||||
'label' => 'assembly.bom.mountnames',
|
||||
'empty_data' => '',
|
||||
'attr' => [
|
||||
'class' => 'tagsinput',
|
||||
'data-controller' => 'elements--tagsinput',
|
||||
]
|
||||
])
|
||||
->add('comment', RichTextEditorType::class, [
|
||||
'required' => false,
|
||||
'label' => 'assembly.bom.comment',
|
||||
'empty_data' => '',
|
||||
'mode' => 'markdown-single_line',
|
||||
'attr' => [
|
||||
'rows' => 2,
|
||||
],
|
||||
])
|
||||
->add('price', BigDecimalNumberType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'scale' => 5,
|
||||
'html5' => true,
|
||||
'attr' => [
|
||||
'min' => 0,
|
||||
'step' => 'any',
|
||||
],
|
||||
])
|
||||
->add('priceCurrency', CurrencyEntityType::class, [
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
'short' => true,
|
||||
])
|
||||
|
||||
;
|
||||
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => AssemblyBOMEntry::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
183
src/Form/AssemblySystem/AssemblyBuildType.php
Normal file
183
src/Form/AssemblySystem/AssemblyBuildType.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\Form\AssemblySystem;
|
||||
|
||||
use App\Helpers\Assemblies\AssemblyBuildRequest;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Form\Type\PartLotSelectType;
|
||||
use App\Form\Type\SIUnitType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\DataMapperInterface;
|
||||
use Symfony\Component\Form\Event\PreSetDataEvent;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AssemblyBuildType extends AbstractType implements DataMapperInterface
|
||||
{
|
||||
public function __construct(private readonly Security $security)
|
||||
{
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'compound' => true,
|
||||
'data_class' => AssemblyBuildRequest::class
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setDataMapper($this);
|
||||
|
||||
$builder->add('submit', SubmitType::class, [
|
||||
'label' => 'assembly.build.btn_build',
|
||||
'disabled' => !$this->security->isGranted('@parts_stock.withdraw'),
|
||||
]);
|
||||
|
||||
$builder->add('dontCheckQuantity', CheckboxType::class, [
|
||||
'label' => 'assembly.build.dont_check_quantity',
|
||||
'help' => 'assembly.build.dont_check_quantity.help',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'data-controller' => 'pages--dont-check-quantity-checkbox'
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->add('comment', TextType::class, [
|
||||
'label' => 'part.info.withdraw_modal.comment',
|
||||
'help' => 'part.info.withdraw_modal.comment.hint',
|
||||
'empty_data' => '',
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
|
||||
//The form is initially empty, we have to define the fields after we know the data
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
|
||||
$form = $event->getForm();
|
||||
/** @var AssemblyBuildRequest $build_request */
|
||||
$build_request = $event->getData();
|
||||
|
||||
$form->add('addBuildsToBuildsPart', CheckboxType::class, [
|
||||
'label' => 'assembly.build.add_builds_to_builds_part',
|
||||
'required' => false,
|
||||
'disabled' => !$build_request->getAssembly()->getBuildPart() instanceof Part,
|
||||
]);
|
||||
|
||||
if ($build_request->getAssembly()->getBuildPart() instanceof Part) {
|
||||
$form->add('buildsPartLot', PartLotSelectType::class, [
|
||||
'label' => 'assembly.build.builds_part_lot',
|
||||
'required' => false,
|
||||
'part' => $build_request->getAssembly()->getBuildPart(),
|
||||
'placeholder' => 'assembly.build.buildsPartLot.new_lot'
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($build_request->getPartBomEntries() as $bomEntry) {
|
||||
//Every part lot has a field to specify the number of parts to take from this lot
|
||||
foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) {
|
||||
$form->add('lot_' . $lot->getID(), SIUnitType::class, [
|
||||
'label' => false,
|
||||
'measurement_unit' => $bomEntry->getPart()->getPartUnit(),
|
||||
'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()),
|
||||
'disabled' => !$this->security->isGranted('withdraw', $lot),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function mapDataToForms($data, \Traversable $forms): void
|
||||
{
|
||||
if (!$data instanceof AssemblyBuildRequest) {
|
||||
throw new \RuntimeException('Data must be an instance of ' . AssemblyBuildRequest::class);
|
||||
}
|
||||
|
||||
/** @var FormInterface[] $forms */
|
||||
$forms = iterator_to_array($forms);
|
||||
foreach ($forms as $key => $form) {
|
||||
//Extract the lot id from the form name
|
||||
$matches = [];
|
||||
if (preg_match('/^lot_(\d+)$/', $key, $matches)) {
|
||||
$lot_id = (int) $matches[1];
|
||||
$form->setData($data->getLotWithdrawAmount($lot_id));
|
||||
}
|
||||
}
|
||||
|
||||
$forms['comment']->setData($data->getComment());
|
||||
$forms['dontCheckQuantity']->setData($data->isDontCheckQuantity());
|
||||
$forms['addBuildsToBuildsPart']->setData($data->getAddBuildsToBuildsPart());
|
||||
if (isset($forms['buildsPartLot'])) {
|
||||
$forms['buildsPartLot']->setData($data->getBuildsPartLot());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function mapFormsToData(\Traversable $forms, &$data): void
|
||||
{
|
||||
if (!$data instanceof AssemblyBuildRequest) {
|
||||
throw new \RuntimeException('Data must be an instance of ' . AssemblyBuildRequest::class);
|
||||
}
|
||||
|
||||
/** @var FormInterface[] $forms */
|
||||
$forms = iterator_to_array($forms);
|
||||
|
||||
foreach ($forms as $key => $form) {
|
||||
//Extract the lot id from the form name
|
||||
$matches = [];
|
||||
if (preg_match('/^lot_(\d+)$/', $key, $matches)) {
|
||||
$lot_id = (int) $matches[1];
|
||||
$data->setLotWithdrawAmount($lot_id, (float) $form->getData());
|
||||
}
|
||||
}
|
||||
|
||||
$data->setComment($forms['comment']->getData());
|
||||
$data->setDontCheckQuantity($forms['dontCheckQuantity']->getData());
|
||||
|
||||
if (isset($forms['buildsPartLot'])) {
|
||||
$lot = $forms['buildsPartLot']->getData();
|
||||
if (!$lot) { //When the user selected "Create new lot", create a new lot
|
||||
$lot = new PartLot();
|
||||
$description = 'Build ' . date('Y-m-d H:i:s');
|
||||
if ($data->getComment() !== '') {
|
||||
$description .= ' (' . $data->getComment() . ')';
|
||||
}
|
||||
$lot->setDescription($description);
|
||||
|
||||
$data->getAssembly()->getBuildPart()->addPartLot($lot);
|
||||
}
|
||||
|
||||
$data->setBuildsPartLot($lot);
|
||||
}
|
||||
//This has to be set after the builds part lot, so that it can disable the option
|
||||
$data->setAddBuildsToBuildsPart($forms['addBuildsToBuildsPart']->getData());
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
|||
namespace App\Form\Filters;
|
||||
|
||||
use App\DataTables\Filters\AttachmentFilter;
|
||||
use App\Entity\Attachments\AssemblyAttachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\AttachmentTypeAttachment;
|
||||
use App\Entity\Attachments\CategoryAttachment;
|
||||
|
|
@ -80,6 +81,7 @@ class AttachmentFilterType extends AbstractType
|
|||
'category.label' => CategoryAttachment::class,
|
||||
'currency.label' => CurrencyAttachment::class,
|
||||
'project.label' => ProjectAttachment::class,
|
||||
'assembly.label' => AssemblyAttachment::class,
|
||||
'footprint.label' => FootprintAttachment::class,
|
||||
'group.label' => GroupAttachment::class,
|
||||
'label_profile.label' => LabelAttachment::class,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class ProjectAddPartsType extends AbstractType
|
|||
],
|
||||
'constraints' => [
|
||||
new UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part']),
|
||||
new UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly']),
|
||||
new UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name']),
|
||||
]
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Form\ProjectSystem;
|
||||
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Form\Type\AssemblySelectType;
|
||||
use App\Form\Type\BigDecimalNumberType;
|
||||
use App\Form\Type\CurrencyEntityType;
|
||||
use App\Form\Type\PartSelectType;
|
||||
|
|
@ -22,8 +23,6 @@ class ProjectBOMEntryType extends AbstractType
|
|||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
|
||||
$form = $event->getForm();
|
||||
/** @var ProjectBOMEntry $data */
|
||||
|
|
@ -36,11 +35,14 @@ class ProjectBOMEntryType extends AbstractType
|
|||
});
|
||||
|
||||
$builder
|
||||
|
||||
->add('part', PartSelectType::class, [
|
||||
'label' => 'project.bom.part',
|
||||
'required' => false,
|
||||
])
|
||||
->add('assembly', AssemblySelectType::class, [
|
||||
'label' => 'project.bom.assembly',
|
||||
'required' => false,
|
||||
])
|
||||
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'project.bom.name',
|
||||
'required' => false,
|
||||
|
|
@ -77,10 +79,7 @@ class ProjectBOMEntryType extends AbstractType
|
|||
'required' => false,
|
||||
'label' => false,
|
||||
'short' => true,
|
||||
])
|
||||
|
||||
;
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\Form\ProjectSystem;
|
||||
|
||||
use App\Helpers\Assemblies\AssemblyBuildRequest;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
|
|
@ -38,10 +39,11 @@ use Symfony\Component\Form\FormBuilderInterface;
|
|||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ProjectBuildType extends AbstractType implements DataMapperInterface
|
||||
{
|
||||
public function __construct(private readonly Security $security)
|
||||
public function __construct(private readonly Security $security, private readonly TranslatorInterface $translator)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -82,36 +84,54 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface
|
|||
//The form is initially empty, we have to define the fields after we know the data
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
|
||||
$form = $event->getForm();
|
||||
/** @var ProjectBuildRequest $build_request */
|
||||
$build_request = $event->getData();
|
||||
/** @var ProjectBuildRequest $projectBuildRequest */
|
||||
$projectBuildRequest = $event->getData();
|
||||
|
||||
$form->add('addBuildsToBuildsPart', CheckboxType::class, [
|
||||
'label' => 'project.build.add_builds_to_builds_part',
|
||||
'required' => false,
|
||||
'disabled' => !$build_request->getProject()->getBuildPart() instanceof Part,
|
||||
'disabled' => !$projectBuildRequest->getProject()->getBuildPart() instanceof Part,
|
||||
]);
|
||||
|
||||
if ($build_request->getProject()->getBuildPart() instanceof Part) {
|
||||
if ($projectBuildRequest->getProject()->getBuildPart() instanceof Part) {
|
||||
$form->add('buildsPartLot', PartLotSelectType::class, [
|
||||
'label' => 'project.build.builds_part_lot',
|
||||
'required' => false,
|
||||
'part' => $build_request->getProject()->getBuildPart(),
|
||||
'part' => $projectBuildRequest->getProject()->getBuildPart(),
|
||||
'placeholder' => 'project.build.buildsPartLot.new_lot'
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($build_request->getPartBomEntries() as $bomEntry) {
|
||||
foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) {
|
||||
//Every part lot has a field to specify the number of parts to take from this lot
|
||||
foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) {
|
||||
foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $lot) {
|
||||
$form->add('lot_' . $lot->getID(), SIUnitType::class, [
|
||||
'label' => false,
|
||||
'measurement_unit' => $bomEntry->getPart()->getPartUnit(),
|
||||
'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()),
|
||||
'max' => min($projectBuildRequest->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()),
|
||||
'disabled' => !$this->security->isGranted('withdraw', $lot),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) {
|
||||
$assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds());
|
||||
|
||||
//Add fields for assembly bom entries
|
||||
foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) {
|
||||
foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) {
|
||||
$form->add('lot_' . $lot->getID(), SIUnitType::class, [
|
||||
'label' => $this->translator->trans('project.build.builds_part_lot_label', [
|
||||
'%name%' => $partBomEntry->getPart()->getName(),
|
||||
'%quantity%' => $partBomEntry->getQuantity() * $projectBuildRequest->getNumberOfBuilds()
|
||||
]),
|
||||
'measurement_unit' => $partBomEntry->getPart()->getPartUnit(),
|
||||
'max' => min($assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry), $lot->getAmount()),
|
||||
'disabled' => !$this->security->isGranted('withdraw', $lot),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
125
src/Form/Type/AssemblySelectType.php
Normal file
125
src/Form/Type/AssemblySelectType.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?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',
|
||||
'placeholder' => 'None',
|
||||
'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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ class PartSelectType extends AbstractType implements DataMapperInterface
|
|||
$options = $form->get('autocomplete')->getConfig()->getOptions();
|
||||
|
||||
|
||||
if (!isset($data['autocomplete']) || '' === $data['autocomplete']) {
|
||||
if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) {
|
||||
$options['choices'] = [];
|
||||
} else {
|
||||
//Extract the ID from the submitted data
|
||||
|
|
@ -84,7 +84,6 @@ class PartSelectType extends AbstractType implements DataMapperInterface
|
|||
'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']),
|
||||
//Disable browser autocomplete
|
||||
'autocomplete' => 'off',
|
||||
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
@ -103,7 +102,7 @@ class PartSelectType extends AbstractType implements DataMapperInterface
|
|||
}
|
||||
|
||||
return $part instanceof Part ? [
|
||||
'data-description' => mb_strimwidth($part->getDescription(), 0, 127, '...'),
|
||||
'data-description' => $part->getDescription() ? mb_strimwidth($part->getDescription(), 0, 127, '...') : '',
|
||||
'data-category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : '',
|
||||
'data-footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '',
|
||||
'data-image' => $preview_url,
|
||||
|
|
|
|||
306
src/Helpers/Assemblies/AssemblyBuildRequest.php
Normal file
306
src/Helpers/Assemblies/AssemblyBuildRequest.php
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\Helpers\Assemblies;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Validator\Constraints\AssemblySystem\ValidAssemblyBuildRequest;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Helpers\Assemblies\AssemblyBuildRequestTest
|
||||
*/
|
||||
#[ValidAssemblyBuildRequest]
|
||||
final class AssemblyBuildRequest
|
||||
{
|
||||
private readonly int $number_of_builds;
|
||||
|
||||
/**
|
||||
* @var array<int, float>
|
||||
*/
|
||||
private array $withdraw_amounts = [];
|
||||
|
||||
private string $comment = '';
|
||||
|
||||
private ?PartLot $builds_lot = null;
|
||||
|
||||
private bool $add_build_to_builds_part = false;
|
||||
|
||||
private bool $dont_check_quantity = false;
|
||||
|
||||
/**
|
||||
* @param Assembly $assembly The assembly that should be build
|
||||
* @param int $number_of_builds The number of builds that should be created
|
||||
*/
|
||||
public function __construct(private readonly Assembly $assembly, int $number_of_builds)
|
||||
{
|
||||
if ($number_of_builds < 1) {
|
||||
throw new \InvalidArgumentException('Number of builds must be at least 1!');
|
||||
}
|
||||
$this->number_of_builds = $number_of_builds;
|
||||
|
||||
$this->initializeArray();
|
||||
|
||||
//By default, use the first available lot of builds part if there is one.
|
||||
if($assembly->getBuildPart() instanceof Part) {
|
||||
$this->add_build_to_builds_part = true;
|
||||
foreach( $assembly->getBuildPart()->getPartLots() as $lot) {
|
||||
if (!$lot->isInstockUnknown()) {
|
||||
$this->builds_lot = $lot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeArray(): void
|
||||
{
|
||||
//Completely reset the array
|
||||
$this->withdraw_amounts = [];
|
||||
|
||||
//Now create an array for each BOM entry
|
||||
foreach ($this->getPartBomEntries() as $bom_entry) {
|
||||
$remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry);
|
||||
foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) {
|
||||
//If the lot has instock use it for the build
|
||||
$this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount());
|
||||
$remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the assemblyBOMEntry belongs to the assembly, otherwise throw an exception.
|
||||
*/
|
||||
private function ensureBOMEntryValid(AssemblyBOMEntry $entry): void
|
||||
{
|
||||
if ($entry->getAssembly() !== $this->assembly) {
|
||||
throw new \InvalidArgumentException('The given BOM entry does not belong to the assembly!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the partlot where the builds should be added to, or null if it should not be added to any lot.
|
||||
*/
|
||||
public function getBuildsPartLot(): ?PartLot
|
||||
{
|
||||
return $this->builds_lot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the builds should be added to the builds part of this assembly as new stock
|
||||
*/
|
||||
public function getAddBuildsToBuildsPart(): bool
|
||||
{
|
||||
return $this->add_build_to_builds_part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the builds should be added to the builds part of this assembly as new stock
|
||||
* @return $this
|
||||
*/
|
||||
public function setAddBuildsToBuildsPart(bool $new_value): self
|
||||
{
|
||||
$this->add_build_to_builds_part = $new_value;
|
||||
|
||||
if ($new_value === false) {
|
||||
$this->builds_lot = null;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the partlot where the builds should be added to, or null if it should not be added to any lot.
|
||||
* The part lot must belong to the assembly build part, or an exception is thrown!
|
||||
* @return $this
|
||||
*/
|
||||
public function setBuildsPartLot(?PartLot $new_part_lot): self
|
||||
{
|
||||
//Ensure that this new_part_lot belongs to the assembly
|
||||
if (($new_part_lot instanceof PartLot && $new_part_lot->getPart() !== $this->assembly->getBuildPart()) || !$this->assembly->getBuildPart() instanceof Part) {
|
||||
throw new \InvalidArgumentException('The given part lot does not belong to the assemblies build part!');
|
||||
}
|
||||
|
||||
if ($new_part_lot instanceof PartLot) {
|
||||
$this->setAddBuildsToBuildsPart(true);
|
||||
}
|
||||
|
||||
$this->builds_lot = $new_part_lot;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the comment where the user can write additional information about the build.
|
||||
*/
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the comment where the user can write additional information about the build.
|
||||
*/
|
||||
public function setComment(string $comment): void
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry.
|
||||
* @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got
|
||||
*/
|
||||
public function getLotWithdrawAmount(PartLot|int $lot): float
|
||||
{
|
||||
$lot_id = $lot instanceof PartLot ? $lot->getID() : $lot;
|
||||
|
||||
if (! array_key_exists($lot_id, $this->withdraw_amounts)) {
|
||||
throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!');
|
||||
}
|
||||
|
||||
return $this->withdraw_amounts[$lot_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry.
|
||||
* @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got
|
||||
* @return $this
|
||||
*/
|
||||
public function setLotWithdrawAmount(PartLot|int $lot, float $amount): self
|
||||
{
|
||||
if ($lot instanceof PartLot) {
|
||||
$lot_id = $lot->getID();
|
||||
} elseif (is_int($lot)) {
|
||||
$lot_id = $lot;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!');
|
||||
}
|
||||
|
||||
$this->withdraw_amounts[$lot_id] = $amount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of all withdraw amounts for the given BOM entry.
|
||||
*/
|
||||
public function getWithdrawAmountSum(AssemblyBOMEntry $entry): float
|
||||
{
|
||||
$this->ensureBOMEntryValid($entry);
|
||||
|
||||
$sum = 0;
|
||||
foreach ($this->getPartLotsForBOMEntry($entry) as $lot) {
|
||||
$sum += $this->getLotWithdrawAmount($lot);
|
||||
}
|
||||
|
||||
if ($entry->getPart() && !$entry->getPart()->useFloatAmount()) {
|
||||
$sum = round($sum);
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of available lots to take stock from for the given BOM entry.
|
||||
* @return PartLot[]|null Returns null if the entry is a non-part BOM entry
|
||||
*/
|
||||
public function getPartLotsForBOMEntry(AssemblyBOMEntry $assemblyBOMEntry): ?array
|
||||
{
|
||||
$this->ensureBOMEntryValid($assemblyBOMEntry);
|
||||
|
||||
if (!$assemblyBOMEntry->getPart() instanceof Part) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Filter out all lots which have unknown instock
|
||||
return $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the needed amount of parts for the given BOM entry.
|
||||
*/
|
||||
public function getNeededAmountForBOMEntry(AssemblyBOMEntry $entry): float
|
||||
{
|
||||
$this->ensureBOMEntryValid($entry);
|
||||
|
||||
return $entry->getQuantity() * $this->number_of_builds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of all bom entries.
|
||||
* @return AssemblyBOMEntry[]
|
||||
*/
|
||||
public function getBomEntries(): array
|
||||
{
|
||||
return $this->assembly->getBomEntries()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all part bom entries.
|
||||
* @return AssemblyBOMEntry[]
|
||||
*/
|
||||
public function getPartBomEntries(): array
|
||||
{
|
||||
return $this->assembly->getBomEntries()->filter(fn(AssemblyBOMEntry $entry) => $entry->isPartBomEntry())->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which assembly should be build
|
||||
*/
|
||||
public function getAssembly(): Assembly
|
||||
{
|
||||
return $this->assembly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of builds that should be created.
|
||||
*/
|
||||
public function getNumberOfBuilds(): int
|
||||
{
|
||||
return $this->number_of_builds;
|
||||
}
|
||||
|
||||
/**
|
||||
* If Set to true, the given withdraw amounts are used without any checks for requirements.
|
||||
* @return bool
|
||||
*/
|
||||
public function isDontCheckQuantity(): bool
|
||||
{
|
||||
return $this->dont_check_quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true, the given withdraw amounts are used without any checks for requirements.
|
||||
* @param bool $dont_check_quantity
|
||||
* @return $this
|
||||
*/
|
||||
public function setDontCheckQuantity(bool $dont_check_quantity): AssemblyBuildRequest
|
||||
{
|
||||
$this->dont_check_quantity = $dont_check_quantity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -22,10 +22,13 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\Helpers\Projects;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Helpers\Assemblies\AssemblyBuildRequest;
|
||||
use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest;
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +82,7 @@ final class ProjectBuildRequest
|
|||
//Completely reset the array
|
||||
$this->withdraw_amounts = [];
|
||||
|
||||
//Now create an array for each BOM entry
|
||||
//Now create an array for each part BOM entry
|
||||
foreach ($this->getPartBomEntries() as $bom_entry) {
|
||||
$remaining_amount = $this->getNeededAmountForBOMEntry($bom_entry);
|
||||
foreach($this->getPartLotsForBOMEntry($bom_entry) as $lot) {
|
||||
|
|
@ -88,6 +91,21 @@ final class ProjectBuildRequest
|
|||
$remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]);
|
||||
}
|
||||
}
|
||||
|
||||
//Now create an array for each assembly BOM entry
|
||||
foreach ($this->getAssemblyBomEntries() as $assemblyBomEntry) {
|
||||
$assemblyBuildRequest = new AssemblyBuildRequest($assemblyBomEntry->getAssembly(), $this->number_of_builds);
|
||||
|
||||
//Add fields for assembly bom entries
|
||||
foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) {
|
||||
$remaining_amount = $assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry) * $assemblyBomEntry->getQuantity();
|
||||
|
||||
foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) {
|
||||
$this->withdraw_amounts[$lot->getID()] = min($remaining_amount, $lot->getAmount());
|
||||
$remaining_amount -= max(0, $this->withdraw_amounts[$lot->getID()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -230,12 +248,77 @@ final class ProjectBuildRequest
|
|||
{
|
||||
$this->ensureBOMEntryValid($projectBOMEntry);
|
||||
|
||||
if (!$projectBOMEntry->getPart() instanceof Part) {
|
||||
if (!$projectBOMEntry->getPart() instanceof Part && !$projectBOMEntry->getAssembly() instanceof Assembly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Filter out all lots which have unknown instock
|
||||
return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray();
|
||||
if ($projectBOMEntry->getPart() instanceof Part) {
|
||||
return $projectBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray();
|
||||
} elseif ($projectBOMEntry->getAssembly() instanceof Assembly) {
|
||||
$assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds);
|
||||
|
||||
//Add fields for assembly bom entries
|
||||
$result = [];
|
||||
foreach ($assemblyBuildRequest->getPartBomEntries() as $assemblyBOMEntry) {
|
||||
$tmp = $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->toArray();
|
||||
$result = array_merge($result, $tmp);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available assembly BOM-entries with no part assigned.
|
||||
* @return AssemblyBOMEntry[]|null Returns null if no entries found
|
||||
*/
|
||||
public function getAssemblyBomEntriesWithoutPart(ProjectBOMEntry $projectBOMEntry): ?array
|
||||
{
|
||||
$this->ensureBOMEntryValid($projectBOMEntry);
|
||||
|
||||
if (!$projectBOMEntry->getAssembly() instanceof Assembly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds);
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) {
|
||||
if ($assemblyBOMEntry->getPart() === null) {
|
||||
$result[] = $assemblyBOMEntry;
|
||||
}
|
||||
}
|
||||
|
||||
return count($result) > 0 ? $result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available assembly BOM-entries with no part assigned.
|
||||
* @return AssemblyBOMEntry[]|null Returns null if no entries found
|
||||
*/
|
||||
public function getAssemblyBomEntriesWithPartNoStock(ProjectBOMEntry $projectBOMEntry): ?array
|
||||
{
|
||||
$this->ensureBOMEntryValid($projectBOMEntry);
|
||||
|
||||
if (!$projectBOMEntry->getAssembly() instanceof Assembly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$assemblyBuildRequest = new AssemblyBuildRequest($projectBOMEntry->getAssembly(), $this->number_of_builds);
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($assemblyBuildRequest->getBomEntries() as $assemblyBOMEntry) {
|
||||
if ($assemblyBOMEntry->getPart() instanceof Part && $assemblyBOMEntry->getPart()->getPartLots()->filter(fn (PartLot $lot) => !$lot->isInstockUnknown())->count() === 0) {
|
||||
$result[] = $assemblyBOMEntry;
|
||||
}
|
||||
}
|
||||
|
||||
return count($result) > 0 ? $result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -266,6 +349,15 @@ final class ProjectBuildRequest
|
|||
return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isPartBomEntry())->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the all assembly bom entries that have to be built.
|
||||
* @return ProjectBOMEntry[]
|
||||
*/
|
||||
public function getAssemblyBomEntries(): array
|
||||
{
|
||||
return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isAssemblyBomEntry())->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which project should be build
|
||||
*/
|
||||
|
|
@ -301,6 +393,4 @@ final class ProjectBuildRequest
|
|||
$this->dont_check_quantity = $dont_check_quantity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
69
src/Repository/AssemblyRepository.php
Normal file
69
src/Repository/AssemblyRepository.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -154,4 +154,14 @@ class DBElementRepository extends EntityRepository
|
|||
$property->setAccessible(true);
|
||||
$property->setValue($element, $new_value);
|
||||
}
|
||||
|
||||
protected function save(AbstractDBElement $entity, bool $flush = true): void
|
||||
{
|
||||
$manager = $this->getEntityManager();
|
||||
$manager->persist($entity);
|
||||
|
||||
if ($flush) {
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Security\Voter;
|
||||
|
||||
use App\Entity\Attachments\AssemblyAttachment;
|
||||
use App\Services\UserSystem\VoterHelper;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Entity\Attachments\AttachmentContainingDBElement;
|
||||
|
|
@ -89,6 +90,8 @@ final class AttachmentVoter extends Voter
|
|||
$param = 'currencies';
|
||||
} elseif (is_a($subject, ProjectAttachment::class, true)) {
|
||||
$param = 'projects';
|
||||
} elseif (is_a($subject, AssemblyAttachment::class, true)) {
|
||||
$param = 'assemblies';
|
||||
} elseif (is_a($subject, FootprintAttachment::class, true)) {
|
||||
$param = 'footprints';
|
||||
} elseif (is_a($subject, GroupAttachment::class, true)) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Security\Voter;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\Parts\Category;
|
||||
|
|
@ -47,6 +48,7 @@ final class StructureVoter extends Voter
|
|||
AttachmentType::class => 'attachment_types',
|
||||
Category::class => 'categories',
|
||||
Project::class => 'projects',
|
||||
Assembly::class => 'assemblies',
|
||||
Footprint::class => 'footprints',
|
||||
Manufacturer::class => 'manufacturers',
|
||||
StorageLocation::class => 'storelocations',
|
||||
|
|
|
|||
154
src/Services/AssemblySystem/AssemblyBuildHelper.php
Normal file
154
src/Services/AssemblySystem/AssemblyBuildHelper.php
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\Services\AssemblySystem;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Helpers\Assemblies\AssemblyBuildRequest;
|
||||
use App\Services\Parts\PartLotWithdrawAddHelper;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Services\AssemblySystem\AssemblyBuildHelperTest
|
||||
*/
|
||||
class AssemblyBuildHelper
|
||||
{
|
||||
public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum buildable amount of the given BOM entry based on the stock of the used parts.
|
||||
* This function only works for BOM entries that are associated with a part.
|
||||
*/
|
||||
public function getMaximumBuildableCountForBOMEntry(AssemblyBOMEntry $assemblyBOMEntry): int
|
||||
{
|
||||
$part = $assemblyBOMEntry->getPart();
|
||||
|
||||
if (!$part instanceof Part) {
|
||||
throw new \InvalidArgumentException('This function cannot determine the maximum buildable count for a BOM entry without a part!');
|
||||
}
|
||||
|
||||
if ($assemblyBOMEntry->getQuantity() <= 0) {
|
||||
throw new \RuntimeException('The quantity of the BOM entry must be greater than 0!');
|
||||
}
|
||||
|
||||
$amount_sum = $part->getAmountSum();
|
||||
|
||||
return (int) floor($amount_sum / $assemblyBOMEntry->getQuantity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum buildable amount of the given assembly, based on the stock of the used parts in the BOM.
|
||||
*/
|
||||
public function getMaximumBuildableCount(Assembly $assembly): int
|
||||
{
|
||||
$maximum_buildable_count = PHP_INT_MAX;
|
||||
foreach ($assembly->getBomEntries() as $bom_entry) {
|
||||
//Skip BOM entries without a part (as we can not determine that)
|
||||
if (!$bom_entry->isPartBomEntry()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//The maximum buildable count for the whole assembly is the minimum of all BOM entries
|
||||
$maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry));
|
||||
}
|
||||
|
||||
return $maximum_buildable_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given assembly can be built with the current stock.
|
||||
* This means that the maximum buildable count is greater or equal than the requested $number_of_assemblies
|
||||
* @param int $number_of_builds
|
||||
*/
|
||||
public function isAssemblyBuildable(Assembly $assembly, int $number_of_builds = 1): bool
|
||||
{
|
||||
return $this->getMaximumBuildableCount($assembly) >= $number_of_builds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given BOM entry can be built with the current stock.
|
||||
* This means that the maximum buildable count is greater or equal than the requested $number_of_assemblies
|
||||
*/
|
||||
public function isBOMEntryBuildable(AssemblyBOMEntry $bom_entry, int $number_of_builds = 1): bool
|
||||
{
|
||||
return $this->getMaximumBuildableCountForBOMEntry($bom_entry) >= $number_of_builds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 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?
|
||||
* @return AssemblyBOMEntry[]
|
||||
*/
|
||||
public function getNonBuildableAssemblyBomEntries(Assembly $assembly, int $number_of_builds = 1): array
|
||||
{
|
||||
if ($number_of_builds < 1) {
|
||||
throw new \InvalidArgumentException('The number of builds must be greater than 0!');
|
||||
}
|
||||
|
||||
$non_buildable_entries = [];
|
||||
|
||||
foreach ($assembly->getBomEntries() as $bomEntry) {
|
||||
$part = $bomEntry->getPart();
|
||||
|
||||
//Skip BOM entries without a part (as we can not determine that)
|
||||
if (!$part instanceof Part) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$amount_sum = $part->getAmountSum();
|
||||
|
||||
if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) {
|
||||
$non_buildable_entries[] = $bomEntry;
|
||||
}
|
||||
}
|
||||
|
||||
return $non_buildable_entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw the parts from the stock using the given AssemblyBuildRequest and create the build parts entries, if needed.
|
||||
* The AssemblyBuildRequest has to be validated before!!
|
||||
* You have to flush changes to DB afterward
|
||||
*/
|
||||
public function doBuild(AssemblyBuildRequest $buildRequest): void
|
||||
{
|
||||
$message = $buildRequest->getComment();
|
||||
$message .= ' (Assembly build: '.$buildRequest->getAssembly()->getName().')';
|
||||
|
||||
foreach ($buildRequest->getPartBomEntries() as $bom_entry) {
|
||||
foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) {
|
||||
$amount = $buildRequest->getLotWithdrawAmount($part_lot);
|
||||
if ($amount > 0) {
|
||||
$this->withdraw_add_helper->withdraw($part_lot, $amount, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($buildRequest->getAddBuildsToBuildsPart()) {
|
||||
$this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/Services/AssemblySystem/AssemblyBuildPartHelper.php
Normal file
40
src/Services/AssemblySystem/AssemblyBuildPartHelper.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\AssemblySystem;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Parts\Part;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Services\AssemblySystem\AssemblyBuildPartHelperTest
|
||||
*/
|
||||
class AssemblyBuildPartHelper
|
||||
{
|
||||
/**
|
||||
* Returns a part that represents the builds of a assembly. This part is not saved to the database, and can be used
|
||||
* as initial data for the new part form.
|
||||
*/
|
||||
public function getPartInitialization(Assembly $assembly): Part
|
||||
{
|
||||
$part = new Part();
|
||||
|
||||
//Associate the part with the assembly
|
||||
$part->setBuiltAssembly($assembly);
|
||||
|
||||
//Set the name of the part to the name of the assembly
|
||||
$part->setName($assembly->getName());
|
||||
|
||||
//Set the description of the part to the description of the assembly
|
||||
$part->setDescription($assembly->getDescription());
|
||||
|
||||
//Add a tag to the part that indicates that it is a build part
|
||||
$part->setTags('assembly-build');
|
||||
|
||||
//Associate the part with the assembly
|
||||
$assembly->setBuildPart($part);
|
||||
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
93
src/Services/Attachments/AssemblyPreviewGenerator.php
Normal file
93
src/Services/Attachments/AssemblyPreviewGenerator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services\Attachments;
|
||||
|
||||
use App\Entity\Attachments\AssemblyAttachment;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentContainingDBElement;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
|
|
@ -84,6 +85,7 @@ class AttachmentSubmitHandler
|
|||
CategoryAttachment::class => 'category',
|
||||
CurrencyAttachment::class => 'currency',
|
||||
ProjectAttachment::class => 'project',
|
||||
AssemblyAttachment::class => 'assembly',
|
||||
FootprintAttachment::class => 'footprint',
|
||||
GroupAttachment::class => 'group',
|
||||
ManufacturerAttachment::class => 'manufacturer',
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ use App\Entity\PriceInformations\Orderdetail;
|
|||
use App\Entity\PriceInformations\Pricedetail;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Exceptions\EntityNotSupportedException;
|
||||
|
|
@ -66,6 +68,8 @@ class ElementTypeNameGenerator
|
|||
AttachmentType::class => $this->translator->trans('attachment_type.label'),
|
||||
Project::class => $this->translator->trans('project.label'),
|
||||
ProjectBOMEntry::class => $this->translator->trans('project_bom_entry.label'),
|
||||
Assembly::class => $this->translator->trans('assembly.label'),
|
||||
AssemblyBOMEntry::class => $this->translator->trans('assembly_bom_entry.label'),
|
||||
Footprint::class => $this->translator->trans('footprint.label'),
|
||||
Manufacturer::class => $this->translator->trans('manufacturer.label'),
|
||||
MeasurementUnit::class => $this->translator->trans('measurement_unit.label'),
|
||||
|
|
@ -182,6 +186,8 @@ class ElementTypeNameGenerator
|
|||
$on = $entity->getOrderdetail()->getPart();
|
||||
} elseif ($entity instanceof ProjectBOMEntry && $entity->getProject() instanceof Project) {
|
||||
$on = $entity->getProject();
|
||||
} elseif ($entity instanceof AssemblyBOMEntry && $entity->getAssembly() instanceof Assembly) {
|
||||
$on = $entity->getAssembly();
|
||||
}
|
||||
|
||||
if (isset($on) && $on instanceof NamedElementInterface) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\PartAttachment;
|
||||
|
|
@ -98,6 +99,7 @@ class EntityURLGenerator
|
|||
AttachmentType::class => 'attachment_type_edit',
|
||||
Category::class => 'category_edit',
|
||||
Project::class => 'project_edit',
|
||||
Assembly::class => 'assembly_edit',
|
||||
Supplier::class => 'supplier_edit',
|
||||
Manufacturer::class => 'manufacturer_edit',
|
||||
StorageLocation::class => 'store_location_edit',
|
||||
|
|
@ -204,6 +206,7 @@ class EntityURLGenerator
|
|||
AttachmentType::class => 'attachment_type_edit',
|
||||
Category::class => 'category_edit',
|
||||
Project::class => 'project_info',
|
||||
Assembly::class => 'assembly_info',
|
||||
Supplier::class => 'supplier_edit',
|
||||
Manufacturer::class => 'manufacturer_edit',
|
||||
StorageLocation::class => 'store_location_edit',
|
||||
|
|
@ -234,6 +237,7 @@ class EntityURLGenerator
|
|||
AttachmentType::class => 'attachment_type_edit',
|
||||
Category::class => 'category_edit',
|
||||
Project::class => 'project_edit',
|
||||
Assembly::class => 'assembly_edit',
|
||||
Supplier::class => 'supplier_edit',
|
||||
Manufacturer::class => 'manufacturer_edit',
|
||||
StorageLocation::class => 'store_location_edit',
|
||||
|
|
@ -265,6 +269,7 @@ class EntityURLGenerator
|
|||
AttachmentType::class => 'attachment_type_new',
|
||||
Category::class => 'category_new',
|
||||
Project::class => 'project_new',
|
||||
Assembly::class => 'assembly_new',
|
||||
Supplier::class => 'supplier_new',
|
||||
Manufacturer::class => 'manufacturer_new',
|
||||
StorageLocation::class => 'store_location_new',
|
||||
|
|
@ -296,6 +301,7 @@ class EntityURLGenerator
|
|||
AttachmentType::class => 'attachment_type_clone',
|
||||
Category::class => 'category_clone',
|
||||
Project::class => 'device_clone',
|
||||
Assembly::class => 'assembly_clone',
|
||||
Supplier::class => 'supplier_clone',
|
||||
Manufacturer::class => 'manufacturer_clone',
|
||||
StorageLocation::class => 'store_location_clone',
|
||||
|
|
@ -323,6 +329,7 @@ class EntityURLGenerator
|
|||
{
|
||||
$map = [
|
||||
Project::class => 'project_info',
|
||||
Assembly::class => 'assembly_info',
|
||||
|
||||
Category::class => 'part_list_category',
|
||||
Footprint::class => 'part_list_footprint',
|
||||
|
|
@ -341,6 +348,7 @@ class EntityURLGenerator
|
|||
AttachmentType::class => 'attachment_type_delete',
|
||||
Category::class => 'category_delete',
|
||||
Project::class => 'project_delete',
|
||||
Assembly::class => 'assembly_delete',
|
||||
Supplier::class => 'supplier_delete',
|
||||
Manufacturer::class => 'manufacturer_delete',
|
||||
StorageLocation::class => 'store_location_delete',
|
||||
|
|
|
|||
|
|
@ -22,15 +22,25 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\Services\ImportExportSystem;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Repository\DBElementRepository;
|
||||
use App\Repository\PartRepository;
|
||||
use App\Repository\Parts\CategoryRepository;
|
||||
use App\Repository\Parts\ManufacturerRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use League\Csv\Reader;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use RuntimeException;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Services\ImportExportSystem\BOMImporterTest
|
||||
|
|
@ -47,17 +57,29 @@ class BOMImporter
|
|||
5 => 'Supplier and ref',
|
||||
];
|
||||
|
||||
private readonly PartRepository $partRepository;
|
||||
|
||||
private readonly ManufacturerRepository $manufacturerRepository;
|
||||
|
||||
private readonly CategoryRepository $categoryRepository;
|
||||
|
||||
private readonly DBElementRepository $assemblyBOMEntryRepository;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly BOMValidationService $validationService
|
||||
) {
|
||||
$this->partRepository = $entityManager->getRepository(Part::class);
|
||||
$this->manufacturerRepository = $entityManager->getRepository(Manufacturer::class);
|
||||
$this->categoryRepository = $entityManager->getRepository(Category::class);
|
||||
$this->assemblyBOMEntryRepository = $entityManager->getRepository(AssemblyBOMEntry::class);
|
||||
}
|
||||
|
||||
protected function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||
{
|
||||
$resolver->setRequired('type');
|
||||
$resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic']);
|
||||
$resolver->setAllowedValues('type', ['kicad_pcbnew', 'kicad_schematic', 'json']);
|
||||
|
||||
// For flexible schematic import with field mapping
|
||||
$resolver->setDefined(['field_mapping', 'field_priorities', 'delimiter']);
|
||||
|
|
@ -88,12 +110,29 @@ class BOMImporter
|
|||
}
|
||||
|
||||
/**
|
||||
* Converts the given file into an array of BOM entries using the given options.
|
||||
* @return ProjectBOMEntry[]
|
||||
* Converts the given file into an array of BOM entries using the given options and save them into the given assembly.
|
||||
* The changes are not saved into the database yet.
|
||||
* @return AssemblyBOMEntry[]
|
||||
*/
|
||||
public function fileToBOMEntries(File $file, array $options): array
|
||||
public function importFileIntoAssembly(File $file, Assembly $assembly, array $options): array
|
||||
{
|
||||
return $this->stringToBOMEntries($file->getContent(), $options);
|
||||
$bomEntries = $this->fileToBOMEntries($file, $options, AssemblyBOMEntry::class);
|
||||
|
||||
//Assign the bom_entries to the assembly
|
||||
foreach ($bomEntries as $bom_entry) {
|
||||
$assembly->addBomEntry($bom_entry);
|
||||
}
|
||||
|
||||
return $bomEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given file into an array of BOM entries using the given options.
|
||||
* @return ProjectBOMEntry[]|AssemblyBOMEntry[]
|
||||
*/
|
||||
public function fileToBOMEntries(File $file, array $options, string $objectType = ProjectBOMEntry::class): array
|
||||
{
|
||||
return $this->stringToBOMEntries($file->getContent(), $options, $objectType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,22 +156,22 @@ class BOMImporter
|
|||
* Import string data into an array of BOM entries, which are not yet assigned to a project.
|
||||
* @param string $data The data to import
|
||||
* @param array $options An array of options
|
||||
* @return ProjectBOMEntry[] An array of imported entries
|
||||
* @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries
|
||||
*/
|
||||
public function stringToBOMEntries(string $data, array $options): array
|
||||
public function stringToBOMEntries(string $data, array $options, string $objectType = ProjectBOMEntry::class): array
|
||||
{
|
||||
$resolver = new OptionsResolver();
|
||||
$resolver = $this->configureOptions($resolver);
|
||||
$options = $resolver->resolve($options);
|
||||
|
||||
return match ($options['type']) {
|
||||
'kicad_pcbnew' => $this->parseKiCADPCB($data),
|
||||
'kicad_schematic' => $this->parseKiCADSchematic($data, $options),
|
||||
'kicad_pcbnew' => $this->parseKiCADPCB($data, $objectType),
|
||||
'json' => $this->parseJson($data, $options, $objectType),
|
||||
default => throw new InvalidArgumentException('Invalid import type!'),
|
||||
};
|
||||
}
|
||||
|
||||
private function parseKiCADPCB(string $data): array
|
||||
private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): array
|
||||
{
|
||||
$csv = Reader::createFromString($data);
|
||||
$csv->setDelimiter(';');
|
||||
|
|
@ -158,8 +197,13 @@ class BOMImporter
|
|||
throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!');
|
||||
}
|
||||
|
||||
$bom_entry = new ProjectBOMEntry();
|
||||
$bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')');
|
||||
$bom_entry = $objectType === ProjectBOMEntry::class ? new ProjectBOMEntry() : new AssemblyBOMEntry();
|
||||
if ($objectType === ProjectBOMEntry::class) {
|
||||
$bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')');
|
||||
} else {
|
||||
$bom_entry->setName($entry['Designation']);
|
||||
}
|
||||
|
||||
$bom_entry->setMountnames($entry['Designator'] ?? '');
|
||||
$bom_entry->setComment($entry['Supplier and ref'] ?? '');
|
||||
$bom_entry->setQuantity((float) ($entry['Quantity'] ?? 1));
|
||||
|
|
@ -227,6 +271,174 @@ class BOMImporter
|
|||
return $this->validationService->validateBOMEntries($mapped_entries, $options);
|
||||
}
|
||||
|
||||
private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$data = json_decode($data, true);
|
||||
|
||||
foreach ($data as $entry) {
|
||||
// Check quantity
|
||||
if (!isset($entry['quantity'])) {
|
||||
throw new UnexpectedValueException('quantity missing');
|
||||
}
|
||||
if (!is_float($entry['quantity']) || $entry['quantity'] <= 0) {
|
||||
throw new UnexpectedValueException('quantity expected as float greater than 0.0');
|
||||
}
|
||||
|
||||
// Check name
|
||||
if (isset($entry['name']) && !is_string($entry['name'])) {
|
||||
throw new UnexpectedValueException('name of part list entry expected as string');
|
||||
}
|
||||
|
||||
// Check if part is assigned with relevant information
|
||||
if (isset($entry['part'])) {
|
||||
if (!is_array($entry['part'])) {
|
||||
throw new UnexpectedValueException('The property "part" should be an array');
|
||||
}
|
||||
|
||||
$partIdValid = isset($entry['part']['id']) && is_int($entry['part']['id']) && $entry['part']['id'] > 0;
|
||||
$partNameValid = isset($entry['part']['name']) && is_string($entry['part']['name']) && trim($entry['part']['name']) !== '';
|
||||
$partMpnrValid = isset($entry['part']['mpnr']) && is_string($entry['part']['mpnr']) && trim($entry['part']['mpnr']) !== '';
|
||||
$partIpnValid = isset($entry['part']['ipn']) && is_string($entry['part']['ipn']) && trim($entry['part']['ipn']) !== '';
|
||||
|
||||
if (!$partIdValid && !$partNameValid && !$partMpnrValid && !$partIpnValid) {
|
||||
throw new UnexpectedValueException(
|
||||
'The property "part" must have either assigned: "id" as integer greater than 0, "name", "mpnr", or "ipn" as non-empty string'
|
||||
);
|
||||
}
|
||||
|
||||
$part = $partIdValid ? $this->partRepository->findOneBy(['id' => $entry['part']['id']]) : null;
|
||||
$part = $part ?? ($partMpnrValid ? $this->partRepository->findOneBy(['manufacturer_product_number' => trim($entry['part']['mpnr'])]) : null);
|
||||
$part = $part ?? ($partIpnValid ? $this->partRepository->findOneBy(['ipn' => trim($entry['part']['ipn'])]) : null);
|
||||
$part = $part ?? ($partNameValid ? $this->partRepository->findOneBy(['name' => trim($entry['part']['name'])]) : null);
|
||||
|
||||
if ($part === null) {
|
||||
$part = new Part();
|
||||
$part->setName($entry['part']['name']);
|
||||
}
|
||||
|
||||
if ($partNameValid && $part->getName() !== trim($entry['part']['name'])) {
|
||||
throw new RuntimeException(sprintf('Part name does not match exact the given name. Given for import: %s, found part: %s', $entry['part']['name'], $part->getName()));
|
||||
}
|
||||
|
||||
if ($partIpnValid && $part->getManufacturerProductNumber() !== trim($entry['part']['mpnr'])) {
|
||||
throw new RuntimeException(sprintf('Part mpnr does not match exact the given mpnr. Given for import: %s, found part: %s', $entry['part']['mpnr'], $part->getManufacturerProductNumber()));
|
||||
}
|
||||
|
||||
if ($partIpnValid && $part->getIpn() !== trim($entry['part']['ipn'])) {
|
||||
throw new RuntimeException(sprintf('Part ipn does not match exact the given ipn. Given for import: %s, found part: %s', $entry['part']['ipn'], $part->getIpn()));
|
||||
}
|
||||
|
||||
// Part: Description check
|
||||
if (isset($entry['part']['description']) && !is_null($entry['part']['description'])) {
|
||||
if (!is_string($entry['part']['description']) || trim($entry['part']['description']) === '') {
|
||||
throw new UnexpectedValueException('The property path "part.description" must be a non-empty string if not null');
|
||||
}
|
||||
}
|
||||
$partDescription = $entry['part']['description'] ?? '';
|
||||
|
||||
// Part: Manufacturer check
|
||||
$manufacturerIdValid = false;
|
||||
$manufacturerNameValid = false;
|
||||
if (array_key_exists('manufacturer', $entry['part'])) {
|
||||
if (!is_array($entry['part']['manufacturer'])) {
|
||||
throw new UnexpectedValueException('The property path "part.manufacturer" must be an array');
|
||||
}
|
||||
|
||||
$manufacturerIdValid = isset($entry['part']['manufacturer']['id']) && is_int($entry['part']['manufacturer']['id']) && $entry['part']['manufacturer']['id'] > 0;
|
||||
$manufacturerNameValid = isset($entry['part']['manufacturer']['name']) && is_string($entry['part']['manufacturer']['name']) && trim($entry['part']['manufacturer']['name']) !== '';
|
||||
|
||||
// Stellen sicher, dass mindestens eine Bedingung für manufacturer erfüllt sein muss
|
||||
if (!$manufacturerIdValid && !$manufacturerNameValid) {
|
||||
throw new UnexpectedValueException(
|
||||
'The property "manufacturer" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$manufacturer = $manufacturerIdValid ? $this->manufacturerRepository->findOneBy(['id' => $entry['part']['manufacturer']['id']]) : null;
|
||||
$manufacturer = $manufacturer ?? ($manufacturerNameValid ? $this->manufacturerRepository->findOneBy(['name' => trim($entry['part']['manufacturer']['name'])]) : null);
|
||||
|
||||
if ($manufacturer === null) {
|
||||
throw new RuntimeException(
|
||||
'Manufacturer not found'
|
||||
);
|
||||
}
|
||||
|
||||
if ($manufacturerNameValid && $manufacturer->getName() !== trim($entry['part']['manufacturer']['name'])) {
|
||||
throw new RuntimeException(sprintf('Manufacturer name does not match exact the given name. Given for import: %s, found manufacturer: %s', $entry['manufacturer']['name'], $manufacturer->getName()));
|
||||
}
|
||||
|
||||
// Part: Category check
|
||||
$categoryIdValid = false;
|
||||
$categoryNameValid = false;
|
||||
if (array_key_exists('category', $entry['part'])) {
|
||||
if (!is_array($entry['part']['category'])) {
|
||||
throw new UnexpectedValueException('part.category must be an array');
|
||||
}
|
||||
|
||||
$categoryIdValid = isset($entry['part']['category']['id']) && is_int($entry['part']['category']['id']) && $entry['part']['category']['id'] > 0;
|
||||
$categoryNameValid = isset($entry['part']['category']['name']) && is_string($entry['part']['category']['name']) && trim($entry['part']['category']['name']) !== '';
|
||||
|
||||
if (!$categoryIdValid && !$categoryNameValid) {
|
||||
throw new UnexpectedValueException(
|
||||
'The property "category" must have either assigned: "id" as integer greater than 0, or "name" as non-empty string'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$category = $categoryIdValid ? $this->categoryRepository->findOneBy(['id' => $entry['part']['category']['id']]) : null;
|
||||
$category = $category ?? ($categoryNameValid ? $this->categoryRepository->findOneBy(['name' => trim($entry['part']['category']['name'])]) : null);
|
||||
|
||||
if ($category === null) {
|
||||
throw new RuntimeException(
|
||||
'Category not found'
|
||||
);
|
||||
}
|
||||
|
||||
if ($categoryNameValid && $category->getName() !== trim($entry['part']['category']['name'])) {
|
||||
throw new RuntimeException(sprintf('Category name does not match exact the given name. Given for import: %s, found category: %s', $entry['category']['name'], $category->getName()));
|
||||
}
|
||||
|
||||
$part->setDescription($partDescription);
|
||||
$part->setManufacturer($manufacturer);
|
||||
$part->setCategory($category);
|
||||
|
||||
if ($partMpnrValid) {
|
||||
$part->setManufacturerProductNumber($entry['part']['mpnr'] ?? '');
|
||||
}
|
||||
if ($partIpnValid) {
|
||||
$part->setIpn($entry['part']['ipn'] ?? '');
|
||||
}
|
||||
|
||||
if ($objectType === AssemblyBOMEntry::class) {
|
||||
$bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]);
|
||||
|
||||
if ($bomEntry === null) {
|
||||
$name = isset($entry['name']) && $entry['name'] !== null ? trim($entry['name']) : '';
|
||||
$bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]);
|
||||
|
||||
if ($bomEntry === null) {
|
||||
$bomEntry = new AssemblyBOMEntry();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$bomEntry = new ProjectBOMEntry();
|
||||
}
|
||||
|
||||
$bomEntry->setQuantity($entry['quantity']);
|
||||
$bomEntry->setName($entry['name'] ?? '');
|
||||
|
||||
$bomEntry->setPart($part);
|
||||
}
|
||||
|
||||
$result[] = $bomEntry;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function uses the order of the fields in the CSV files to make them locale independent.
|
||||
* @param array $entry
|
||||
|
|
@ -243,7 +455,7 @@ class BOMImporter
|
|||
}
|
||||
|
||||
//@phpstan-ignore-next-line We want to keep this check just to be safe when something changes
|
||||
$new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new \UnexpectedValueException('Invalid field index!');
|
||||
$new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new UnexpectedValueException('Invalid field index!');
|
||||
$out[$new_index] = $field;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,13 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\Services\ProjectSystem;
|
||||
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Helpers\Assemblies\AssemblyBuildRequest;
|
||||
use App\Helpers\Projects\ProjectBuildRequest;
|
||||
use App\Services\AssemblySystem\AssemblyBuildHelper;
|
||||
use App\Services\Parts\PartLotWithdrawAddHelper;
|
||||
|
||||
/**
|
||||
|
|
@ -33,8 +36,10 @@ use App\Services\Parts\PartLotWithdrawAddHelper;
|
|||
*/
|
||||
class ProjectBuildHelper
|
||||
{
|
||||
public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PartLotWithdrawAddHelper $withdrawAddHelper,
|
||||
private readonly AssemblyBuildHelper $assemblyBuildHelper
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -66,12 +71,16 @@ class ProjectBuildHelper
|
|||
$maximum_buildable_count = PHP_INT_MAX;
|
||||
foreach ($project->getBomEntries() as $bom_entry) {
|
||||
//Skip BOM entries without a part (as we can not determine that)
|
||||
if (!$bom_entry->isPartBomEntry()) {
|
||||
if (!$bom_entry->isPartBomEntry() && $bom_entry->getAssembly() === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//The maximum buildable count for the whole project is the minimum of all BOM entries
|
||||
$maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry));
|
||||
if ($bom_entry->getPart() !== null) {
|
||||
$maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry));
|
||||
} elseif ($bom_entry->getAssembly() !== null) {
|
||||
$maximum_buildable_count = min($maximum_buildable_count, $this->assemblyBuildHelper->getMaximumBuildableCount($bom_entry->getAssembly()));
|
||||
}
|
||||
}
|
||||
|
||||
return $maximum_buildable_count;
|
||||
|
|
@ -97,10 +106,10 @@ class ProjectBuildHelper
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the project BOM entries for which parts are missing in the stock for the given number of builds
|
||||
* Returns the project or assembly BOM entries for which parts are missing in the stock for the given number of builds
|
||||
* @param Project $project The project for which the BOM entries should be checked
|
||||
* @param int $number_of_builds How often should the project be build?
|
||||
* @return ProjectBOMEntry[]
|
||||
* @return ProjectBOMEntry[]|AssemblyBOMEntry[]
|
||||
*/
|
||||
public function getNonBuildableProjectBomEntries(Project $project, int $number_of_builds = 1): array
|
||||
{
|
||||
|
|
@ -108,24 +117,29 @@ class ProjectBuildHelper
|
|||
throw new \InvalidArgumentException('The number of builds must be greater than 0!');
|
||||
}
|
||||
|
||||
$non_buildable_entries = [];
|
||||
$nonBuildableEntries = [];
|
||||
|
||||
foreach ($project->getBomEntries() as $bomEntry) {
|
||||
$part = $bomEntry->getPart();
|
||||
|
||||
//Skip BOM entries without a part (as we can not determine that)
|
||||
if (!$part instanceof Part) {
|
||||
if (!$part instanceof Part && $bomEntry->getAssembly() === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$amount_sum = $part->getAmountSum();
|
||||
if ($bomEntry->getPart() !== null) {
|
||||
$amount_sum = $part->getAmountSum();
|
||||
|
||||
if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) {
|
||||
$non_buildable_entries[] = $bomEntry;
|
||||
if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) {
|
||||
$nonBuildableEntries[] = $bomEntry;
|
||||
}
|
||||
} elseif ($bomEntry->getAssembly() !== null) {
|
||||
$nonBuildableAssemblyEntries = $this->assemblyBuildHelper->getNonBuildableAssemblyBomEntries($bomEntry->getAssembly(), $number_of_builds);
|
||||
$nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries);
|
||||
}
|
||||
}
|
||||
|
||||
return $non_buildable_entries;
|
||||
return $nonBuildableEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -133,22 +147,37 @@ class ProjectBuildHelper
|
|||
* The ProjectBuildRequest has to be validated before!!
|
||||
* You have to flush changes to DB afterward
|
||||
*/
|
||||
public function doBuild(ProjectBuildRequest $buildRequest): void
|
||||
public function doBuild(ProjectBuildRequest $projectBuildRequest): void
|
||||
{
|
||||
$message = $buildRequest->getComment();
|
||||
$message .= ' (Project build: '.$buildRequest->getProject()->getName().')';
|
||||
$message = $projectBuildRequest->getComment();
|
||||
$message .= ' (Project build: '.$projectBuildRequest->getProject()->getName().')';
|
||||
|
||||
foreach ($buildRequest->getPartBomEntries() as $bom_entry) {
|
||||
foreach ($buildRequest->getPartLotsForBOMEntry($bom_entry) as $part_lot) {
|
||||
$amount = $buildRequest->getLotWithdrawAmount($part_lot);
|
||||
foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) {
|
||||
foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $partLot) {
|
||||
$amount = $projectBuildRequest->getLotWithdrawAmount($partLot);
|
||||
if ($amount > 0) {
|
||||
$this->withdraw_add_helper->withdraw($part_lot, $amount, $message);
|
||||
$this->withdrawAddHelper->withdraw($partLot, $amount, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($buildRequest->getAddBuildsToBuildsPart()) {
|
||||
$this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message);
|
||||
foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) {
|
||||
$assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds());
|
||||
|
||||
//Add fields for assembly bom entries
|
||||
foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) {
|
||||
foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $partLot) {
|
||||
//Read amount from build configuration of the projectBuildRequest
|
||||
$amount = $projectBuildRequest->getLotWithdrawAmount($partLot);
|
||||
if ($amount > 0) {
|
||||
$this->withdrawAddHelper->withdraw($partLot, $amount, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($projectBuildRequest->getAddBuildsToBuildsPart()) {
|
||||
$this->withdrawAddHelper->add($projectBuildRequest->getBuildsPartLot(), $projectBuildRequest->getNumberOfBuilds(), $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services\Trees;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Entity\Parts\Category;
|
||||
|
|
@ -175,6 +176,12 @@ class ToolsTreeBuilder
|
|||
$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->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->translator->trans('tree.tools.edit.suppliers'),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services\Trees;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
|
|
@ -154,6 +155,10 @@ class TreeViewGenerator
|
|||
$href_type = 'list_parts';
|
||||
}
|
||||
|
||||
if ($mode === 'assemblies') {
|
||||
$href_type = 'list_parts';
|
||||
}
|
||||
|
||||
$generic = $this->getGenericTree($class, $parent);
|
||||
$treeIterator = new TreeViewNodeIterator($generic);
|
||||
$recursiveIterator = new RecursiveIteratorIterator($treeIterator, RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
|
@ -219,6 +224,7 @@ class TreeViewGenerator
|
|||
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'),
|
||||
};
|
||||
}
|
||||
|
|
@ -233,6 +239,7 @@ class TreeViewGenerator
|
|||
Manufacturer::class => $icon.'fa-industry',
|
||||
Supplier::class => $icon.'fa-truck',
|
||||
Project::class => $icon.'fa-archive',
|
||||
Assembly::class => $icon.'fa-list',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
|
|
@ -108,6 +109,7 @@ final class EntityExtension extends AbstractExtension
|
|||
Manufacturer::class => 'manufacturer',
|
||||
Category::class => 'category',
|
||||
Project::class => 'device',
|
||||
Assembly::class => 'assembly',
|
||||
Attachment::class => 'attachment',
|
||||
Supplier::class => 'supplier',
|
||||
User::class => 'user',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\Validator\Constraints\AssemblySystem;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* This constraint checks that the given ValidAssemblyBuildRequest is valid.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class ValidAssemblyBuildRequest extends Constraint
|
||||
{
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\Validator\Constraints\AssemblySystem;
|
||||
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Helpers\Assemblies\AssemblyBuildRequest;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
|
||||
|
||||
class ValidAssemblyBuildRequestValidator extends ConstraintValidator
|
||||
{
|
||||
private function buildViolationForLot(PartLot $partLot, string $message): ConstraintViolationBuilderInterface
|
||||
{
|
||||
return $this->context->buildViolation($message)
|
||||
->atPath('lot_' . $partLot->getID())
|
||||
->setParameter('{{ lot }}', $partLot->getName());
|
||||
}
|
||||
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof ValidAssemblyBuildRequest) {
|
||||
throw new UnexpectedTypeException($constraint, ValidAssemblyBuildRequest::class);
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$value instanceof AssemblyBuildRequest) {
|
||||
throw new UnexpectedTypeException($value, AssemblyBuildRequest::class);
|
||||
}
|
||||
|
||||
foreach ($value->getPartBomEntries() as $bom_entry) {
|
||||
$withdraw_sum = $value->getWithdrawAmountSum($bom_entry);
|
||||
$needed_amount = $value->getNeededAmountForBOMEntry($bom_entry);
|
||||
|
||||
foreach ($value->getPartLotsForBOMEntry($bom_entry) as $lot) {
|
||||
$withdraw_amount = $value->getLotWithdrawAmount($lot);
|
||||
|
||||
if ($withdraw_amount < 0) {
|
||||
$this->buildViolationForLot($lot, 'validator.assembly_build.lot_must_not_smaller_0')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($withdraw_amount > $lot->getAmount()) {
|
||||
$this->buildViolationForLot($lot, 'validator.assembly_build.lot_must_not_bigger_than_stock')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($withdraw_sum > $needed_amount && $value->isDontCheckQuantity() === false) {
|
||||
$this->buildViolationForLot($lot, 'validator.assembly_build.lot_bigger_than_needed')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($withdraw_sum < $needed_amount && $value->isDontCheckQuantity() === false) {
|
||||
$this->buildViolationForLot($lot, 'validator.assembly_build.lot_smaller_than_needed')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue