mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-13 05:39:33 +00:00
Assemblies einführen
This commit is contained in:
parent
e1418dfdc1
commit
6fa960df42
107 changed files with 14101 additions and 96 deletions
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue