Added way to batch edit the location of parts with a single stock

This commit is contained in:
Jan Böhmer 2025-03-29 16:21:10 +01:00
parent 68124a340b
commit 48c70c3bb4
5 changed files with 120 additions and 36 deletions

View file

@ -29,6 +29,7 @@ use App\DataTables\PartsDataTable;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Exceptions\InvalidRegexException;
@ -43,8 +44,11 @@ use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatorInterface;
use function Symfony\Component\Translation\t;
class PartListsController extends AbstractController
{
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly NodesListBuilder $nodesListBuilder, private readonly DataTableFactory $dataTableFactory, private readonly TranslatorInterface $translator)
@ -71,13 +75,32 @@ class PartListsController extends AbstractController
if (null === $action || null === $ids) {
$this->addFlash('error', 'part.table.actions.no_params_given');
} else {
$errors = [];
$parts = $actionHandler->idStringToArray($ids);
$redirectResponse = $actionHandler->handleAction($action, $parts, $target ? (int) $target : null, $redirect);
$redirectResponse = $actionHandler->handleAction($action, $parts, $target ? (int) $target : null, $redirect, $errors);
//Save changes
$this->entityManager->flush();
$this->addFlash('success', 'part.table.actions.success');
if (count($errors) === 0) {
$this->addFlash('success', 'part.table.actions.success');
} else {
$this->addFlash('error', t('part.table.actions.error', ['%count%' => count($errors)]));
//Create a flash message for each error
foreach ($errors as $error) {
/** @var Part $part */
$part = $error['part'];
$this->addFlash('error',
t('part.table.actions.error_detail', [
'%part_name%' => $part->getName(),
'%part_id%' => $part->getID(),
'%message%' => $error['message']
])
);
}
}
}
//If the action handler returned a response, we use it, otherwise we redirect back to the previous page.

View file

@ -29,6 +29,7 @@ use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\StorageLocation;
use App\Entity\ProjectSystem\Project;
use App\Form\Type\Helper\StructuralEntityChoiceHelper;
use App\Services\Trees\NodesListBuilder;
@ -78,6 +79,12 @@ class SelectAPIController extends AbstractController
return $this->getResponseForClass(Project::class, false);
}
#[Route(path: '/storage_location', name: 'select_storage_location')]
public function locations(): Response
{
return $this->getResponseForClass(StorageLocation::class, true);
}
#[Route(path: '/export_level', name: 'select_export_level')]
public function exportLevel(): Response
{

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
*/
namespace App\Services\Parts;
use App\Entity\Parts\StorageLocation;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
@ -35,6 +36,9 @@ use InvalidArgumentException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Contracts\Translation\TranslatableInterface;
use function Symfony\Component\Translation\t;
final class PartsTableActionHandler
{
@ -61,8 +65,9 @@ final class PartsTableActionHandler
/**
* @param Part[] $selected_parts
* @return RedirectResponse|null Returns a redirect response if the user should be redirected to another page, otherwise null
* @phpstan-param-out array<array{'part': Part, 'message': string|TranslatableInterface}> $errors
*/
public function handleAction(string $action, array $selected_parts, ?int $target_id, ?string $redirect_url = null): ?RedirectResponse
public function handleAction(string $action, array $selected_parts, ?int $target_id, ?string $redirect_url = null, array &$errors = []): ?RedirectResponse
{
if ($action === 'add_to_project') {
return new RedirectResponse(
@ -161,6 +166,29 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart
$this->denyAccessUnlessGranted('@measurement_units.read');
$part->setPartUnit(null === $target_id ? null : $this->entityManager->find(MeasurementUnit::class, $target_id));
break;
case 'change_location':
$this->denyAccessUnlessGranted('@storelocations.read');
//Retrieve the first part lot and set the location for it
$part_lots = $part->getPartLots();
if ($part_lots->count() > 0) {
if ($part_lots->count() > 1) {
$errors[] = [
'part' => $part,
'message' => t('parts.table.action_handler.error.part_lots_multiple'),
];
break;
}
$part_lot = $part_lots->first();
$part_lot->setStorageLocation(null === $target_id ? null : $this->entityManager->find(StorageLocation::class, $target_id));
} else { //Create a new part lot if there are none
$part_lot = new PartLot();
$part_lot->setPart($part);
$part_lot->setInstockUnknown(true); //We do not know how many parts are in stock, so we set it to true
$part_lot->setStorageLocation(null === $target_id ? null : $this->entityManager->find(StorageLocation::class, $target_id));
$this->entityManager->persist($part_lot);
}
break;
default:
throw new InvalidArgumentException('The given action is unknown! ('.$action.')');