diff --git a/migrations/Version20221218192108.php b/migrations/Version20221218192108.php index 8de3b04d..df5cb87e 100644 --- a/migrations/Version20221218192108.php +++ b/migrations/Version20221218192108.php @@ -20,7 +20,7 @@ final class Version20221218192108 extends AbstractMigration public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE device_parts ADD name LONGTEXT NOT NULL, ADD comment LONGTEXT NOT NULL, ADD last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, ADD datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE quantity quantity DOUBLE PRECISION NOT NULL'); + $this->addSql('ALTER TABLE device_parts ADD name VARCHAR(255) NULL, ADD comment LONGTEXT NOT NULL, ADD last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, ADD datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE quantity quantity DOUBLE PRECISION NOT NULL'); $this->addSql('ALTER TABLE devices ADD description LONGTEXT NOT NULL'); } diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index 6b82454d..a54c6d65 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -29,6 +29,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use InvalidArgumentException; +use Symfony\Component\Validator\Constraints as Assert; /** * Class AttachmentType. @@ -53,6 +54,7 @@ class Project extends AbstractStructuralDBElement /** * @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="project", cascade={"persist", "remove"}, orphanRemoval=true) + * @Assert\Valid() */ protected $bom_entries; diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index ad2e5af1..053aea35 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -26,7 +26,9 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; use App\Entity\Parts\Part; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * The ProjectBOMEntry class represents a entry in a project's BOM. @@ -34,6 +36,8 @@ use Symfony\Component\Validator\Constraints as Assert; * @ORM\Table("device_parts") * @ORM\HasLifecycleCallbacks() * @ORM\Entity() + * @UniqueEntity(fields={"part", "project"}, message="project.bom_entry.part_already_in_bom") + * @UniqueEntity(fields={"name", "project"}, message="project.bom_entry.name_already_in_bom", ignoreNull=true) */ class ProjectBOMEntry extends AbstractDBElement { @@ -42,7 +46,7 @@ class ProjectBOMEntry extends AbstractDBElement /** * @var int * @ORM\Column(type="float", name="quantity") - * @Assert\PositiveOrZero() + * @Assert\Positive() */ protected float $quantity; @@ -54,9 +58,13 @@ class ProjectBOMEntry extends AbstractDBElement /** * @var string An optional name describing this BOM entry (useful for non-part entries) - * @ORM\Column(type="text") + * @ORM\Column(type="string", nullable=true) + * @Assert\Expression( + * "this.getPart() !== null or this.getName() !== null", + * message="validator.project.bom_entry.name_or_part_needed" + * ) */ - protected string $name; + protected ?string $name = null; /** * @var string An optional comment for this BOM entry @@ -117,7 +125,7 @@ class ProjectBOMEntry extends AbstractDBElement /** * @return string */ - public function getName(): string + public function getName(): ?string { return $this->name; } @@ -126,7 +134,7 @@ class ProjectBOMEntry extends AbstractDBElement * @param string $name * @return ProjectBOMEntry */ - public function setName(string $name): ProjectBOMEntry + public function setName(?string $name): ProjectBOMEntry { $this->name = $name; return $this; @@ -188,5 +196,37 @@ class ProjectBOMEntry extends AbstractDBElement return $this; } + /** + * @Assert\Callback + */ + public function validate(ExecutionContextInterface $context, $payload) + { + //Round quantity to whole numbers, if the part is not a decimal part + if ($this->part) { + if (!$this->part->getPartUnit() || $this->part->getPartUnit()->isInteger()) { + $this->quantity = round($this->quantity); + } + } + + //Check that every part name in the mountnames list is unique (per bom_entry) + $mountnames = explode(',', $this->mountnames); + $mountnames = array_map('trim', $mountnames); + $uniq_mountnames = array_unique($mountnames); + + //If the number of unique names is not the same as the number of names, there are duplicates + if (count($mountnames) !== count($uniq_mountnames)) { + $context->buildViolation('project.bom_entry.mountnames_not_unique') + ->atPath('mountnames') + ->addViolation(); + } + + //Check that the number of mountnames is the same as the (rounded) quantity + if (!empty($this->mountnames) && count($uniq_mountnames) !== (int) round ($this->quantity)) { + $context->buildViolation('project.bom_entry.mountnames_quantity_mismatch') + ->atPath('mountnames') + ->addViolation(); + } + } + } diff --git a/src/Form/ProjectSystem/ProjectBOMEntryType.php b/src/Form/ProjectSystem/ProjectBOMEntryType.php index 81ec9329..774eb6b1 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -5,12 +5,15 @@ namespace App\Form\ProjectSystem; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Form\Type\PartSelectType; +use App\Form\Type\SIUnitType; use Svg\Tag\Text; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Event\PreSetDataEvent; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; class ProjectBOMEntryType extends AbstractType @@ -18,11 +21,20 @@ class ProjectBOMEntryType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { - $builder - ->add('quantity', NumberType::class, [ + + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { + $form = $event->getForm(); + /** @var ProjectBOMEntry $data */ + $data = $event->getData(); + + $form->add('quantity', SIUnitType::class, [ 'label' => 'project.bom.quantity', - ]) + 'measurement_unit' => $data && $data->getPart() ? $data->getPart()->getPartUnit() : null, + ]); + }); + + $builder ->add('part', PartSelectType::class, [ 'required' => false, @@ -31,7 +43,6 @@ class ProjectBOMEntryType extends AbstractType ->add('name', TextType::class, [ 'label' => 'project.bom.name', 'required' => false, - 'empty_data' => '' ]) ->add('mountnames', TextType::class, [ 'required' => false, diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 242e1f2d..913baae9 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9959,5 +9959,17 @@ Element 3 Mount names + + + project.bom.name + Name + + + + + project.bom.comment + Notes + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index f425756a..3eaef9eb 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -239,5 +239,29 @@ The internal part number must be unique. {{ value }} is already in use! + + + validator.project.bom_entry.name_or_part_needed + You have to choose a part for a part BOM entry or set a name for a non-part BOM entry. + + + + + project.bom_entry.name_already_in_bom + There is already an BOM entry with this name! + + + + + project.bom_entry.part_already_in_bom + This part already exists in the BOM! + + + + + project.bom_entry.mountnames_quantity_mismatch + The number of mountnames has to match the BOMs quantity! + +