Part-DB-server/src/Entity/Parts/StorageLocation.php

326 lines
13 KiB
PHP
Raw Normal View History

<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
2022-11-29 22:28:53 +01:00
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
2020-02-22 18:14:36 +01:00
* 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
2020-02-22 18:14:36 +01:00
* GNU Affero General Public License for more details.
*
2020-02-22 18:14:36 +01:00
* 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\Parts;
2024-06-22 00:31:43 +02:00
use Doctrine\Common\Collections\Criteria;
2024-03-03 19:57:31 +01:00
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;
2023-09-04 23:05:30 +02:00
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\McpTool;
use ApiPlatform\Metadata\McpToolCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
2023-10-25 20:40:41 +02:00
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
use App\Mcp\DTO\ElementByIdInput;
use App\Mcp\DTO\StructuralElementOverview;
use App\Mcp\DTO\StructuralElementSearchInput;
2023-06-11 14:55:06 +02:00
use App\Repository\Parts\StorelocationRepository;
use App\State\Mcp\GetStructuralElementDetailsProcessor;
use App\State\Mcp\ListStructuralElementsProcessor;
2023-06-11 14:55:06 +02:00
use Doctrine\DBAL\Types\Types;
use Doctrine\Common\Collections\ArrayCollection;
use App\Entity\Attachments\StorageLocationAttachment;
use App\Entity\Base\AbstractPartsContainingDBElement;
2023-04-15 22:25:03 +02:00
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parameters\StorageLocationParameter;
use App\Entity\UserSystem\User;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
2023-03-12 01:12:35 +01:00
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
2023-06-13 10:36:34 +02:00
* This entity represents a storage location, where parts can be stored.
* @extends AbstractPartsContainingDBElement<StorageLocationAttachment, StorageLocationParameter>
*/
2023-06-11 14:55:06 +02:00
#[ORM\Entity(repositoryClass: StorelocationRepository::class)]
#[ORM\Table('`storelocations`')]
2024-03-03 19:57:31 +01:00
#[ORM\Index(columns: ['name'], name: 'location_idx_name')]
#[ORM\Index(columns: ['parent_id', 'name'], name: 'location_idx_parent_name')]
#[ApiResource(
operations: [
new Get(security: 'is_granted("read", object)'),
new GetCollection(security: 'is_granted("@storelocations.read")'),
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
2023-10-25 20:40:41 +02:00
new GetCollection(
uriTemplate: '/storage_locations/{id}/children.{_format}',
uriVariables: ['id' => new Link(fromProperty: 'children', fromClass: StorageLocation::class)],
2023-10-25 20:40:41 +02:00
openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'),
security: 'is_granted("@storelocations.read")'
),
],
normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['location:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
mcp: [
'list_storage_locations' => new McpToolCollection(
title: 'List/search storage locations',
description: 'List all storage locations, optionally filtered by a keyword matched against the name and comment. Storage locations describe where parts are physically stored. Each entry includes its full hierarchical path, and results are sorted by that path so parents are immediately followed by their own children, making it easy to derive the tree structure from the flat list.',
annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false],
output: StructuralElementOverview::class,
normalizationContext: ['groups' => ['mcp_structural_overview:read']],
input: StructuralElementSearchInput::class,
security: 'is_granted("@storelocations.read")',
processor: ListStructuralElementsProcessor::class,
),
'get_storage_location_details' => new McpTool(
title: 'Get storage location details by ID',
description: 'Get detailed information about a specific storage location by its database ID.',
annotations: ['readOnlyHint' => true, 'destructiveHint' => false, 'idempotentHint' => true, 'openWorldHint' => false],
normalizationContext: ['groups' => ['location:read', 'api:basic:read']],
input: ElementByIdInput::class,
security: 'is_granted("@storelocations.read")',
validate: true,
processor: GetStructuralElementDetailsProcessor::class,
),
],
)]
#[ApiFilter(PropertyFilter::class)]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])]
2024-03-03 19:57:31 +01:00
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
class StorageLocation extends AbstractPartsContainingDBElement
{
2024-03-03 19:57:31 +01:00
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
2024-06-22 00:31:43 +02:00
#[ORM\OrderBy(['name' => Criteria::ASC])]
2023-04-15 22:25:03 +02:00
protected Collection $children;
2023-06-13 20:24:54 +02:00
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
#[ORM\JoinColumn(name: 'parent_id')]
2023-09-04 23:05:30 +02:00
#[Groups(['location:read', 'location:write'])]
#[ApiProperty(readableLink: false, writableLink: false)]
protected ?AbstractStructuralDBElement $parent = null;
2023-09-04 23:05:30 +02:00
#[Groups(['location:read', 'location:write'])]
protected string $comment = '';
2020-01-05 22:49:00 +01:00
/**
* @var MeasurementUnit|null The measurement unit, which parts can be stored in here
*/
2023-06-13 20:24:54 +02:00
#[ORM\ManyToOne(targetEntity: MeasurementUnit::class)]
#[ORM\JoinColumn(name: 'storage_type_id')]
2023-09-04 23:05:30 +02:00
#[Groups(['location:read', 'location:write'])]
2022-09-18 22:59:31 +02:00
protected ?MeasurementUnit $storage_type = null;
2020-01-05 22:49:00 +01:00
/** @var Collection<int, StorageLocationParameter>
*/
2023-05-28 01:21:05 +02:00
#[Assert\Valid]
2024-03-03 19:57:31 +01:00
#[ORM\OneToMany(mappedBy: 'element', targetEntity: StorageLocationParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
2024-06-22 00:31:43 +02:00
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
#[Groups(['location:read', 'location:write'])]
2023-04-15 22:25:03 +02:00
protected Collection $parameters;
/**
* @var bool When this attribute is set, it is not possible to add additional parts or increase the instock of existing parts.
*/
2023-09-04 23:05:30 +02:00
#[Groups(['full', 'import', 'location:read', 'location:write'])]
2023-06-11 14:55:06 +02:00
#[ORM\Column(type: Types::BOOLEAN)]
2022-09-18 22:59:31 +02:00
protected bool $is_full = false;
/**
* @var bool When this property is set, only one part (but many instock) is allowed to be stored in this store location.
*/
2023-09-04 23:05:30 +02:00
#[Groups(['full', 'import', 'location:read', 'location:write'])]
2023-06-11 14:55:06 +02:00
#[ORM\Column(type: Types::BOOLEAN)]
2022-09-18 22:59:31 +02:00
protected bool $only_single_part = false;
/**
* @var bool When this property is set, it is only possible to increase the instock of parts, that are already stored here.
*/
2023-09-04 23:05:30 +02:00
#[Groups(['full', 'import', 'location:read', 'location:write'])]
2023-06-11 14:55:06 +02:00
#[ORM\Column(type: Types::BOOLEAN)]
2022-09-18 22:59:31 +02:00
protected bool $limit_to_existing_parts = false;
2023-03-13 00:52:22 +01:00
/**
* @var User|null The owner of this storage location
*/
2023-05-28 01:21:05 +02:00
#[Assert\Expression('this.getOwner() == null or this.getOwner().isAnonymousUser() === false', message: 'validator.part_lot.owner_must_not_be_anonymous')]
2023-06-11 14:55:06 +02:00
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'id_owner', onDelete: 'SET NULL')]
2023-09-04 23:05:30 +02:00
#[Groups(['location:read', 'location:write'])]
protected ?User $owner = null;
/**
* @var bool If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here.
*/
2023-06-11 14:55:06 +02:00
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
2023-09-04 23:05:30 +02:00
#[Groups(['location:read', 'location:write'])]
protected bool $part_owner_must_match = false;
/**
* @var Collection<int, StorageLocationAttachment>
*/
2023-05-28 01:21:05 +02:00
#[Assert\Valid]
2024-03-03 19:57:31 +01:00
#[ORM\OneToMany(mappedBy: 'element', targetEntity: StorageLocationAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
2023-09-04 23:05:30 +02:00
#[Groups(['location:read', 'location:write'])]
2023-04-15 22:25:03 +02:00
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: StorageLocationAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
2023-09-04 23:05:30 +02:00
#[Groups(['location:read', 'location:write'])]
protected ?Attachment $master_picture_attachment = null;
#[Groups(['location:read'])]
protected ?\DateTimeImmutable $addedDate = null;
#[Groups(['location:read'])]
protected ?\DateTimeImmutable $lastModified = null;
/********************************************************************************
*
* Getters
*
*********************************************************************************/
/**
* Get the "is full" attribute.
*
* When this attribute is set, it is not possible to add additional parts or increase the instock of existing parts.
*
2019-11-10 14:00:56 +01:00
* @return bool * true if the store location is full
* * false if the store location isn't full
*/
public function isFull(): bool
{
2022-12-18 19:45:04 +01:00
return $this->is_full;
}
/**
* When this property is set, only one part (but many instock) is allowed to be stored in this store location.
*/
public function isOnlySinglePart(): bool
{
return $this->only_single_part;
}
public function setOnlySinglePart(bool $only_single_part): self
{
$this->only_single_part = $only_single_part;
return $this;
}
/**
* When this property is set, it is only possible to increase the instock of parts, that are already stored here.
*/
public function isLimitToExistingParts(): bool
{
return $this->limit_to_existing_parts;
}
public function setLimitToExistingParts(bool $limit_to_existing_parts): self
{
$this->limit_to_existing_parts = $limit_to_existing_parts;
return $this;
}
public function getStorageType(): ?MeasurementUnit
{
return $this->storage_type;
}
public function setStorageType(?MeasurementUnit $storage_type): self
{
$this->storage_type = $storage_type;
return $this;
}
/**
* Returns the owner of this storage location
*/
public function getOwner(): ?User
{
return $this->owner;
}
/**
* Sets the owner of this storage location
*/
public function setOwner(?User $owner): StorageLocation
{
$this->owner = $owner;
return $this;
}
/**
* If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here.
*/
public function isPartOwnerMustMatch(): bool
{
return $this->part_owner_must_match;
}
/**
* If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here.
*/
public function setPartOwnerMustMatch(bool $part_owner_must_match): StorageLocation
{
$this->part_owner_must_match = $part_owner_must_match;
return $this;
}
/********************************************************************************
*
* Setters
*
*********************************************************************************/
/**
2019-11-10 14:00:56 +01:00
* Change the "is full" attribute of this store location.
*
* "is_full" = true means that there is no more space in this storelocation.
* This attribute is only for information, it has no effect.
*
* @param bool $new_is_full * true means that the storelocation is full
* * false means that the storelocation isn't full
*/
public function setIsFull(bool $new_is_full): self
{
$this->is_full = $new_is_full;
return $this;
}
public function __construct()
{
parent::__construct();
2023-06-11 14:55:06 +02:00
$this->children = new ArrayCollection();
$this->parameters = new ArrayCollection();
$this->attachments = new ArrayCollection();
}
}