From 4af3cdb7ab903bc1014b3f063b24eeac4f89fc4f Mon Sep 17 00:00:00 2001 From: Dave Branton Date: Fri, 22 May 2026 14:48:54 +1200 Subject: [PATCH 1/4] Basic multi-build implementation. --- config/permissions.yaml | 2 + src/Controller/ToolsController.php | 65 ++++++++++++++++++ .../ProjectSystem/ProjectMultiBuildType.php | 68 +++++++++++++++++++ .../Projects/ProjectMultiBuildRequest.php | 39 +++++++++++ src/Services/Trees/ToolsTreeBuilder.php | 6 ++ .../tools/multi_build/multi_build.html.twig | 28 ++++++++ 6 files changed, 208 insertions(+) create mode 100644 src/Form/ProjectSystem/ProjectMultiBuildType.php create mode 100644 src/Helpers/Projects/ProjectMultiBuildRequest.php create mode 100644 templates/tools/multi_build/multi_build.html.twig diff --git a/config/permissions.yaml b/config/permissions.yaml index 39e91b57e..1aa0c43e2 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -163,6 +163,8 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co label: "tools.builtin_footprints_viewer.title" ic_logos: label: "perm.tools.ic_logos" + multi_build: + label: "tools.multi_build.title" info_providers: label: "perm.part.info_providers" diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index 76dffb4d0..b75f4ee1c 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -31,9 +31,14 @@ use App\Services\System\UpdateAvailableFacade; use App\Settings\AppSettings; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Runtime\SymfonyRuntime; +use Doctrine\ORM\EntityManagerInterface; +use App\Entity\ProjectSystem\Project; +use App\Form\ProjectSystem\ProjectMultiBuildType; +use Psr\Log\LoggerInterface; #[Route(path: '/tools')] class ToolsController extends AbstractController @@ -122,6 +127,66 @@ public function builtInFootprintsViewer(BuiltinAttachmentsFinder $builtinAttachm ]); } + #[Route(path: '/multi_build', name: 'tools_multi_build')] + public function multiBuild(LoggerInterface $logger, EntityManagerInterface $entityManager, Request $request): Response + { + //$this->denyAccessUnlessGranted('@tools.multi_build'); + $all_projects = $entityManager->getRepository(Project::class)->findAll(); + + $form = $this->createForm(ProjectMultiBuildType::class, [], ['projects'=>$all_projects]); + + $form->handleRequest($request); + $combined_bom=[]; + $needs_ordering = []; + $can_build = true; + if ($form->isSubmitted()) + { + if ($form->isValid()) + { + $submitted = "YES"; + foreach($all_projects as $p) + { + $count_for_project = $form->get($p->getID() . "_project")->getData() + 0; + $logger->Info("QTY", ['count_for_project'=>$count_for_project, 'name'=>$p->getName()]); + if ($count_for_project > 0) + { + $logger->Info("BOMSIZE", ['bomsize'=>count($p->getBomEntries())]); + foreach ($p->getBomEntries() as $bom_entry) + { + $part_id = $bom_entry->getPart()->getID(); + $logger->Info("ISAPART", ['q'=>$bom_entry->getQuantity(), 'name'=>$bom_entry->getPart()->getName()]); + if (array_key_exists($part_id, $combined_bom)) + { + $combined_bom[$part_id]['quantity'] += $bom_entry->getQuantity() * $count_for_project; + } + else + { + $combined_bom[$part_id] = array('quantity'=>$bom_entry->getQuantity() * $count_for_project, 'part'=>$bom_entry->getPart()); + } + } + } + } + + foreach($combined_bom as $cb) + { + $total_instock = $cb['part']->getAmountSum(); + $logger->Info("COMBINED BOM", ['total_instock'=>$total_instock, 'needed'=>$cb['quantity']]); + if ($total_instock < $cb['quantity']) + { + $needs_ordering[] = array('needed'=>($cb['quantity']-$total_instock), 'part'=>$cb['part']); + $can_build = false; + } + } + } + } + + return $this->render('tools/multi_build/multi_build.html.twig', [ + 'form'=>$form, + 'needs_ordering'=>$needs_ordering, + 'can_build'=>$can_build + ]); + } + #[Route(path: '/ic_logos', name: 'tools_ic_logos')] public function icLogos(): Response { diff --git a/src/Form/ProjectSystem/ProjectMultiBuildType.php b/src/Form/ProjectSystem/ProjectMultiBuildType.php new file mode 100644 index 000000000..4a949954f --- /dev/null +++ b/src/Form/ProjectSystem/ProjectMultiBuildType.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\ProjectSystem; + +use App\Services\InfoProviderSystem\ProviderRegistry; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\FormBuilderInterface; +use App\Entity\ProjectSystem\Project; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Psr\Log\LoggerInterface; + +class ProjectMultiBuildType extends AbstractType +{ + private LoggerInterface $logger; + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $this->logger->info("LEN {len}", ['len'=>count($options['projects'])]); + foreach($options['projects'] as $p) + { + $builder->add($p->getID() . '_project', NumberType::class, [ + 'label' => $p->getName(), + 'required' => false, + ],0); + } + + $builder->add('submit', SubmitType::class, [ + 'label' => 'info_providers.search.submit', + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'projects' => [], + ]); + $resolver->setAllowedTypes('projects', 'array'); + } +} diff --git a/src/Helpers/Projects/ProjectMultiBuildRequest.php b/src/Helpers/Projects/ProjectMultiBuildRequest.php new file mode 100644 index 000000000..108d63d02 --- /dev/null +++ b/src/Helpers/Projects/ProjectMultiBuildRequest.php @@ -0,0 +1,39 @@ +. + */ +namespace App\Helpers\Projects; + +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest; + +/** + * @see \App\Tests\Helpers\Projects\ProjectBuildRequestTest + */ +#[ValidProjectBuildRequest] +final class ProjectBuildRequest +{ + + +} diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 6397e3af1..df2fcb2ec 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -131,6 +131,12 @@ protected function getToolsNode(): array $this->urlGenerator->generate('tools_builtin_footprints_viewer') ))->setIcon('fa-treeview fa-fw fa-solid fa-images'); } + if ($this->security->isGranted('@tools.multi_build')) { + $nodes[] = (new TreeViewNode( + "Multi Build", + $this->urlGenerator->generate('tools_multi_build') + ))->setIcon('fa-treeview fa-fw fa-solid fa-images'); + } if ($this->security->isGranted('@tools.ic_logos')) { $nodes[] = (new TreeViewNode( $this->translator->trans('perm.tools.ic_logos'), diff --git a/templates/tools/multi_build/multi_build.html.twig b/templates/tools/multi_build/multi_build.html.twig new file mode 100644 index 000000000..d611bf890 --- /dev/null +++ b/templates/tools/multi_build/multi_build.html.twig @@ -0,0 +1,28 @@ +{% extends "main_card.html.twig" %} + +{% block title %}Multi Build{% endblock %} + + +{% block card_title %} + Multi Build{% endblock %} + +{% block card_body %} + + + + +{{ form(form) }} + +{% endblock %} \ No newline at end of file From 7998918ae45e2ef2da06e8f6015518b8fc2aa3f8 Mon Sep 17 00:00:00 2001 From: Dave Branton Date: Fri, 22 May 2026 15:05:15 +1200 Subject: [PATCH 2/4] Clean up very slightly --- src/Controller/ToolsController.php | 9 +---- .../ProjectSystem/ProjectMultiBuildType.php | 10 ----- .../Projects/ProjectMultiBuildRequest.php | 39 ------------------- 3 files changed, 2 insertions(+), 56 deletions(-) delete mode 100644 src/Helpers/Projects/ProjectMultiBuildRequest.php diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index b75f4ee1c..c65ea3ae2 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -128,7 +128,7 @@ public function builtInFootprintsViewer(BuiltinAttachmentsFinder $builtinAttachm } #[Route(path: '/multi_build', name: 'tools_multi_build')] - public function multiBuild(LoggerInterface $logger, EntityManagerInterface $entityManager, Request $request): Response + public function multiBuild(EntityManagerInterface $entityManager, Request $request): Response { //$this->denyAccessUnlessGranted('@tools.multi_build'); $all_projects = $entityManager->getRepository(Project::class)->findAll(); @@ -142,19 +142,15 @@ public function multiBuild(LoggerInterface $logger, EntityManagerInterface $enti if ($form->isSubmitted()) { if ($form->isValid()) - { - $submitted = "YES"; + { foreach($all_projects as $p) { $count_for_project = $form->get($p->getID() . "_project")->getData() + 0; - $logger->Info("QTY", ['count_for_project'=>$count_for_project, 'name'=>$p->getName()]); if ($count_for_project > 0) { - $logger->Info("BOMSIZE", ['bomsize'=>count($p->getBomEntries())]); foreach ($p->getBomEntries() as $bom_entry) { $part_id = $bom_entry->getPart()->getID(); - $logger->Info("ISAPART", ['q'=>$bom_entry->getQuantity(), 'name'=>$bom_entry->getPart()->getName()]); if (array_key_exists($part_id, $combined_bom)) { $combined_bom[$part_id]['quantity'] += $bom_entry->getQuantity() * $count_for_project; @@ -170,7 +166,6 @@ public function multiBuild(LoggerInterface $logger, EntityManagerInterface $enti foreach($combined_bom as $cb) { $total_instock = $cb['part']->getAmountSum(); - $logger->Info("COMBINED BOM", ['total_instock'=>$total_instock, 'needed'=>$cb['quantity']]); if ($total_instock < $cb['quantity']) { $needs_ordering[] = array('needed'=>($cb['quantity']-$total_instock), 'part'=>$cb['part']); diff --git a/src/Form/ProjectSystem/ProjectMultiBuildType.php b/src/Form/ProjectSystem/ProjectMultiBuildType.php index 4a949954f..ce7235a78 100644 --- a/src/Form/ProjectSystem/ProjectMultiBuildType.php +++ b/src/Form/ProjectSystem/ProjectMultiBuildType.php @@ -25,26 +25,16 @@ use App\Services\InfoProviderSystem\ProviderRegistry; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; use App\Entity\ProjectSystem\Project; use Symfony\Component\OptionsResolver\OptionsResolver; -use Psr\Log\LoggerInterface; class ProjectMultiBuildType extends AbstractType { - private LoggerInterface $logger; - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } public function buildForm(FormBuilderInterface $builder, array $options): void { - $this->logger->info("LEN {len}", ['len'=>count($options['projects'])]); foreach($options['projects'] as $p) { $builder->add($p->getID() . '_project', NumberType::class, [ diff --git a/src/Helpers/Projects/ProjectMultiBuildRequest.php b/src/Helpers/Projects/ProjectMultiBuildRequest.php deleted file mode 100644 index 108d63d02..000000000 --- a/src/Helpers/Projects/ProjectMultiBuildRequest.php +++ /dev/null @@ -1,39 +0,0 @@ -. - */ -namespace App\Helpers\Projects; - -use App\Entity\Parts\Part; -use App\Entity\Parts\PartLot; -use App\Entity\ProjectSystem\Project; -use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest; - -/** - * @see \App\Tests\Helpers\Projects\ProjectBuildRequestTest - */ -#[ValidProjectBuildRequest] -final class ProjectBuildRequest -{ - - -} From bd97bf1c974903db0e666370aa345d68fe03c0d7 Mon Sep 17 00:00:00 2001 From: Dave Branton Date: Fri, 22 May 2026 15:42:34 +1200 Subject: [PATCH 3/4] Sometimes this is null --- src/Controller/ToolsController.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index c65ea3ae2..b74bffcfb 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -150,14 +150,17 @@ public function multiBuild(EntityManagerInterface $entityManager, Request $reque { foreach ($p->getBomEntries() as $bom_entry) { - $part_id = $bom_entry->getPart()->getID(); - if (array_key_exists($part_id, $combined_bom)) + if ($bom_entry->getPart()) { - $combined_bom[$part_id]['quantity'] += $bom_entry->getQuantity() * $count_for_project; - } - else - { - $combined_bom[$part_id] = array('quantity'=>$bom_entry->getQuantity() * $count_for_project, 'part'=>$bom_entry->getPart()); + $part_id = $bom_entry->getPart()->getID(); + if (array_key_exists($part_id, $combined_bom)) + { + $combined_bom[$part_id]['quantity'] += $bom_entry->getQuantity() * $count_for_project; + } + else + { + $combined_bom[$part_id] = array('quantity'=>$bom_entry->getQuantity() * $count_for_project, 'part'=>$bom_entry->getPart()); + } } } } From b1390b481a775cb31aa7d377df5202b8ac1d2633 Mon Sep 17 00:00:00 2001 From: Dave Branton Date: Fri, 22 May 2026 19:13:10 +1200 Subject: [PATCH 4/4] Group by supplier --- src/Controller/ToolsController.php | 31 +++++++++++++++- .../tools/multi_build/multi_build.html.twig | 37 ++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index b74bffcfb..a0795d44a 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -139,6 +139,7 @@ public function multiBuild(EntityManagerInterface $entityManager, Request $reque $combined_bom=[]; $needs_ordering = []; $can_build = true; + $orders_per_supplier = null; if ($form->isSubmitted()) { if ($form->isValid()) @@ -165,14 +166,39 @@ public function multiBuild(EntityManagerInterface $entityManager, Request $reque } } } + + $orders_per_supplier=[]; foreach($combined_bom as $cb) { $total_instock = $cb['part']->getAmountSum(); if ($total_instock < $cb['quantity']) { - $needs_ordering[] = array('needed'=>($cb['quantity']-$total_instock), 'part'=>$cb['part']); $can_build = false; + $suppliers = $cb['part']->getOrderDetails(); + if ($suppliers && count($suppliers) > 0) + { + $mid = $suppliers[0]->getSupplier()->getID(); + $orderable_part=array( + 'part'=>$cb['part'], + 'pn'=>$suppliers[0]->getSupplierPartNr(), + 'needed'=>($cb['quantity']-$total_instock), + 'link'=>$suppliers[0]->getSupplierProductURL()); + if (array_key_exists($mid, $orders_per_supplier)) + { + $orders_per_supplier[$mid]['items'][] = $orderable_part; + } + else + { + $orders_per_supplier[$mid] = array( + 'supplier'=>$suppliers[0]->getSupplier()->getName(), + 'items'=>array($orderable_part)); + } + } + else + { + $needs_ordering[] = array('needed'=>($cb['quantity']-$total_instock), 'part'=>$cb['part']); + } } } } @@ -181,7 +207,8 @@ public function multiBuild(EntityManagerInterface $entityManager, Request $reque return $this->render('tools/multi_build/multi_build.html.twig', [ 'form'=>$form, 'needs_ordering'=>$needs_ordering, - 'can_build'=>$can_build + 'can_build'=>$can_build, + 'orders_per_supplier'=>$orders_per_supplier ]); } diff --git a/templates/tools/multi_build/multi_build.html.twig b/templates/tools/multi_build/multi_build.html.twig index d611bf890..9b3fe8f6d 100644 --- a/templates/tools/multi_build/multi_build.html.twig +++ b/templates/tools/multi_build/multi_build.html.twig @@ -10,7 +10,7 @@ +{% if orders_per_supplier is not null %} + +{% for supplier in orders_per_supplier %} + + + + + + + + + + + + + + + + {% for item in supplier.items %} + + + + + + + {% endfor %} + +{% endfor %} + +
+ {% if supplier.supplier is not null %}{{ supplier.supplier }}{% else %}No supplier{% endif %} +
{% trans %}name.label{% endtrans %}{% trans %}orderdetails.edit.supplierpartnr{% endtrans %}{% trans %}description.label{% endtrans %}Quantity Needed
+ {{ item.part.name }}{{ item.pn }}{{ item.part.description }}{{ item.needed }} +
+{% endif %} {{ form(form) }}