diff --git a/config/parameters.yaml b/config/parameters.yaml index 3584df3c..b0ad2175 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -14,6 +14,8 @@ parameters: partdb.db.emulate_natural_sort: '%env(bool:DATABASE_EMULATE_NATURAL_SORT)%' # If this is set to true, natural sorting is emulated on platforms that do not support it natively. This can be slow on large datasets. + partdb.create_assembly_use_ipn_placeholder_in_name: '%env(bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME)%' # Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving. + ###################################################################################################################### # Users and Privacy ###################################################################################################################### diff --git a/config/services.yaml b/config/services.yaml index be293d38..4fdf18ad 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -164,6 +164,10 @@ services: arguments: $saml_enabled: '%partdb.saml.enabled%' + App\Form\AdminPages\AssemblyAdminForm: + arguments: + $useAssemblyIpnPlaceholder: '%partdb.create_assembly_use_ipn_placeholder_in_name%' + #################################################################################################################### # Table settings #################################################################################################################### diff --git a/docs/configuration.md b/docs/configuration.md index efa3efd3..19899c06 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -141,6 +141,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns are: `name`, `id`, `quantity`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `mountnames`, `instockAmount`, `storageLocations`, `addedDate`, `lastModified`. +* `CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME`: Use an %%ipn%% placeholder in the name of a assembly. Placeholder is replaced with the ipn input while saving. ### History/Eventlog-related settings diff --git a/migrations/Version20250304081039.php b/migrations/Version20250304081039.php index c0fc08d9..ae2d6261 100644 --- a/migrations/Version20250304081039.php +++ b/migrations/Version20250304081039.php @@ -120,6 +120,7 @@ final class Version20250304081039 extends AbstractMultiPlatformMigration last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, status VARCHAR(64) DEFAULT NULL, + ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_5F3832C0727ACA70 FOREIGN KEY (parent_id) REFERENCES assemblies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, @@ -132,6 +133,12 @@ final class Version20250304081039 extends AbstractMultiPlatformMigration $this->addSql(<<<'SQL' CREATE INDEX IDX_5F3832C0EA7100A1 ON assemblies (id_preview_attachment) SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); $this->addSql(<<<'SQL' CREATE TABLE assembly_bom_entries ( diff --git a/migrations/Version20250624095045.php b/migrations/Version20250624095045.php new file mode 100644 index 00000000..d875f1d8 --- /dev/null +++ b/migrations/Version20250624095045.php @@ -0,0 +1,84 @@ +addSql(<<<'SQL' + ALTER TABLE assemblies ADD ipn VARCHAR(100) DEFAULT NULL AFTER status + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e2f180363 TO IDX_8C74887E4AD2039E + SQL); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_5F3832C03D721C14 ON assemblies + SQL); + $this->addSql(<<<'SQL' + DROP INDEX assembly_idx_ipn ON assemblies + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP ipn + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assembly_bom_entries RENAME INDEX idx_8c74887e4ad2039e TO IDX_8C74887E2F180363 + SQL); + } + + public function sqLiteUp(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function sqLiteDown(Schema $schema): void + { + //nothing to do. Done via Version20250304081039 + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE assemblies ADD ipn VARCHAR(100) DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_5F3832C03D721C14 ON assemblies (ipn) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX assembly_idx_ipn ON assemblies (ipn) + SQL); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + DROP INDEX UNIQ_5F3832C03D721C14 + SQL); + $this->addSql(<<<'SQL' + DROP INDEX assembly_idx_ipn + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE assemblies DROP ipn + SQL); + } +} diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index 8c8d7520..c6d9cf20 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Controller\AdminPages; use App\DataTables\LogDataTable; +use App\Entity\AssemblySystem\Assembly; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentUpload; @@ -193,6 +194,15 @@ abstract class BaseAdminController extends AbstractController $entity->setMasterPictureAttachment(null); } + if ($entity instanceof Assembly) { + /* Replace ipn placeholder with the IPN information if applicable. + * The '%%ipn%%' placeholder is automatically inserted into the Name property, + * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, + * to avoid having to insert it manually */ + + $entity->setName(str_ireplace('%%ipn%%', $entity->getIpn(), $entity->getName())); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($entity); @@ -287,6 +297,15 @@ abstract class BaseAdminController extends AbstractController $new_entity->setMasterPictureAttachment(null); } + if ($new_entity instanceof Assembly) { + /* Replace ipn placeholder with the IPN information if applicable. + * The '%%ipn%%' placeholder is automatically inserted into the Name property, + * depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one, + * to avoid having to insert it manually */ + + $new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn(), $new_entity->getName())); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($new_entity); $em->flush(); diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 54305a6f..7897ea36 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -47,8 +47,10 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use InvalidArgumentException; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** @@ -58,6 +60,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; */ #[ORM\Entity] #[ORM\Table(name: 'assemblies')] +#[UniqueEntity(fields: ['ipn'], message: 'assembly.ipn.must_be_unique')] +#[ORM\Index(columns: ['ipn'], name: 'assembly_idx_ipn')] #[ApiResource( operations: [ new Get(security: 'is_granted("read", object)'), @@ -83,7 +87,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; normalizationContext: ['groups' => ['assembly:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] )] #[ApiFilter(PropertyFilter::class)] -#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "ipn"])] #[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Assembly extends AbstractStructuralDBElement { @@ -122,6 +126,14 @@ class Assembly extends AbstractStructuralDBElement #[ORM\Column(type: Types::STRING, length: 64, nullable: true)] protected ?string $status = null; + /** + * @var string|null The internal ipn number of the assembly + */ + #[Assert\Length(max: 100)] + #[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])] + #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] + #[Length(max: 100)] + protected ?string $ipn = null; /** * @var Part|null The (optional) part that represents the builds of this assembly in the stock @@ -301,6 +313,25 @@ class Assembly extends AbstractStructuralDBElement $this->status = $status; } + /** + * Returns the internal part number of the assembly. + * @return string + */ + public function getIpn(): ?string + { + return $this->ipn; + } + + /** + * Sets the internal part number of the assembly. + * @param string $ipn The new IPN of the assembly + */ + public function setIpn(?string $ipn): Assembly + { + $this->ipn = $ipn; + return $this; + } + /** * Checks if this assembly has an associated part representing the builds of this assembly in the stock. */ diff --git a/src/Form/AdminPages/AssemblyAdminForm.php b/src/Form/AdminPages/AssemblyAdminForm.php index be1564d2..0512f64a 100644 --- a/src/Form/AdminPages/AssemblyAdminForm.php +++ b/src/Form/AdminPages/AssemblyAdminForm.php @@ -25,11 +25,22 @@ namespace App\Form\AdminPages; use App\Entity\Base\AbstractNamedDBElement; use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType; use App\Form\Type\RichTextEditorType; +use App\Services\LogSystem\EventCommentNeededHelper; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; class AssemblyAdminForm extends BaseEntityAdminForm { + public function __construct( + protected Security $security, + protected EventCommentNeededHelper $eventCommentNeededHelper, + protected bool $useAssemblyIpnPlaceholder = false + ) { + parent::__construct($security, $eventCommentNeededHelper, $useAssemblyIpnPlaceholder); + } + protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void { $builder->add('description', RichTextEditorType::class, [ @@ -60,5 +71,11 @@ class AssemblyAdminForm extends BaseEntityAdminForm 'assembly.status.archived' => 'archived', ], ]); + + $builder->add('ipn', TextType::class, [ + 'required' => false, + 'empty_data' => null, + 'label' => 'assembly.edit.ipn', + ]); } } diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index e5d69b35..bbc437e3 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -48,8 +48,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class BaseEntityAdminForm extends AbstractType { - public function __construct(protected Security $security, protected EventCommentNeededHelper $eventCommentNeededHelper) - { + public function __construct( + protected Security $security, + protected EventCommentNeededHelper $eventCommentNeededHelper, + protected bool $useAssemblyIpnPlaceholder = false + ) { } public function configureOptions(OptionsResolver $resolver): void @@ -70,6 +73,7 @@ class BaseEntityAdminForm extends AbstractType ->add('name', TextType::class, [ 'empty_data' => '', 'label' => 'name.label', + 'data' => $is_new && $entity instanceof Assembly && $this->useAssemblyIpnPlaceholder ? '%%ipn%%' : $entity->getName(), 'attr' => [ 'placeholder' => 'part.name.placeholder', ], diff --git a/templates/admin/assembly_admin.html.twig b/templates/admin/assembly_admin.html.twig index 57dde7d1..e6a90dc0 100644 --- a/templates/admin/assembly_admin.html.twig +++ b/templates/admin/assembly_admin.html.twig @@ -29,6 +29,7 @@ {% block additional_controls %} {{ form_row(form.description) }} {{ form_row(form.status) }} + {{ form_row(form.ipn) }} {% if entity.id %}