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!
+
+