diff --git a/migrations/Version20240905085300.php b/migrations/Version20240905085300.php new file mode 100644 index 00000000..834444ae --- /dev/null +++ b/migrations/Version20240905085300.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE parts ADD orderamount DOUBLE PRECISION NOT NULL DEFAULT 0, ADD orderDelivery DATETIME'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE `parts` DROP orderamount, DROP orderDelivery'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('ALTER TABLE parts ADD COLUMN orderamount DOUBLE PRECISION NOT NULL DEFAULT 0'); + $this->addSql('ALTER TABLE parts ADD COLUMN orderDelivery DATETIME'); + } + + public function sqLiteDown(Schema $schema): void + { + $error; + // TODO: implement backwards migration for SQlite + } +} diff --git a/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php b/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php index eb96ad33..78102fe1 100644 --- a/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php +++ b/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php @@ -48,9 +48,9 @@ public function apply(QueryBuilder $queryBuilder): void //If value is true, we want to filter for parts with stock < desired stock if ($this->value) { - $queryBuilder->andHaving( $this->property . ' < part.minamount'); + $queryBuilder->andHaving($this->property . ' + part.orderamount < part.minamount'); } else { - $queryBuilder->andHaving($this->property . ' >= part.minamount'); + $queryBuilder->andHaving($this->property . ' + part.orderamount >= part.minamount'); } } } diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index ff98c76f..4f2c9e57 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -58,6 +58,8 @@ class PartFilter implements FilterInterface public readonly TextConstraint $comment; public readonly TagsConstraint $tags; public readonly NumberConstraint $minAmount; + public readonly NumberConstraint $orderAmount; + public readonly DateTimeConstraint $orderDelivery; public readonly BooleanConstraint $favorite; public readonly BooleanConstraint $needsReview; public readonly NumberConstraint $mass; @@ -120,6 +122,8 @@ public function __construct(NodesListBuilder $nodesListBuilder) $this->lastModified = new DateTimeConstraint('part.lastModified'); $this->minAmount = new NumberConstraint('part.minamount'); + $this->orderAmount = new NumberConstraint('part.orderamount'); + $this->orderDelivery = new DateTimeConstraint('part.orderDelivery'); /* We have to use an IntConstraint here because otherwise we get just an empty result list when applying the filter This seems to be related to the fact, that PDO does not have an float parameter type and using string type does not work in this situation (at least in SQLite) TODO: Find a better solution here diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index 28c564b1..afb41269 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -151,6 +151,15 @@ public function configure(DataTable $dataTable, array $options): void 'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format($value, $context->getPartUnit())), ]) + ->add('orderamount', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.orderamount'), + 'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format($value, + $context->getPartUnit())), + ]) + ->add('orderDelivery', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.orderDelivery'), + 'timeFormat' => 'none', + ]) ->add('partUnit', TextColumn::class, [ 'label' => $this->translator->trans('part.table.partUnit'), 'orderField' => 'NATSORT(_partUnit.name)', diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 4addf503..589372d4 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -59,6 +59,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Doctrine\DBAL\Types\Types; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; @@ -99,9 +100,9 @@ #[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])] #[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "tags", "manufacturer_product_number"])] #[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])] -#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])] +#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount", "orderamount"])] #[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] -#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'orderDelivery', 'addedDate', 'lastModified'])] class Part extends AttachmentContainingDBElement { use AdvancedPropertyTrait; @@ -124,6 +125,15 @@ class Part extends AttachmentContainingDBElement #[UniqueObjectCollection(fields: ['name', 'group', 'element'])] protected Collection $parameters; + /** + * @var \DateTimeInterface|null Set a time when the new order will arive. + * Set to null, if there is no known date or no order. + */ + #[Groups(['extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column(name: 'orderDelivery', type: Types::DATETIME_MUTABLE, nullable: true)] + #[Year2038BugWorkaround] + protected ?\DateTimeInterface $orderDelivery = null; + /** ************************************************************* * Overridden properties @@ -214,6 +224,26 @@ public function __clone() parent::__clone(); } + /** + * Gets the expected delivery date of the part. Returns null, if no delivery is due. + */ + public function getOrderDelivery(): ?\DateTimeInterface + { + return $this->orderDelivery; + } + + /** + * Sets the expected delivery date of the part. Set to null, if no delivery is due. + * + * + */ + public function setOrderDelivery(?\DateTimeInterface $orderDelivery): self + { + $this->orderDelivery = $orderDelivery; + + return $this; + } + #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload): void { diff --git a/src/Entity/Parts/PartTraits/InstockTrait.php b/src/Entity/Parts/PartTraits/InstockTrait.php index 08b070f3..e616b7c3 100644 --- a/src/Entity/Parts/PartTraits/InstockTrait.php +++ b/src/Entity/Parts/PartTraits/InstockTrait.php @@ -55,6 +55,14 @@ trait InstockTrait #[ORM\Column(type: Types::FLOAT)] protected float $minamount = 0; + /** + * @var float The number of already ordered units + */ + #[Assert\PositiveOrZero] + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::FLOAT)] + protected float $orderamount = 0; + /** * @var ?MeasurementUnit the unit in which the part's amount is measured */ @@ -137,6 +145,21 @@ public function getMinAmount(): float return round($this->minamount); } + /** + * Get the count of parts which are already ordered. + * If an integer-based part unit is selected, the value will be rounded to integers. + * + * @return float count of parts which are already ordered + */ + public function getOrderAmount(): float + { + if ($this->useFloatAmount()) { + return $this->orderamount; + } + + return round($this->orderamount); + } + /** * Checks if this part uses the float amount . * This setting is based on the part unit (see MeasurementUnit->isInteger()). @@ -158,7 +181,7 @@ public function useFloatAmount(): bool */ public function isNotEnoughInstock(): bool { - return $this->getAmountSum() < $this->getMinAmount(); + return ($this->getAmountSum() + $this->getOrderAmount()) < $this->getMinAmount(); } /** @@ -238,4 +261,19 @@ public function setMinAmount(float $new_minamount): self return $this; } + + /** + * Set the amount of already ordered parts. + * See getPartUnit() for the associated unit. + * + * @param float $new_orderamount the new count of parts are already ordered + * + * @return $this + */ + public function setOrderAmount(float $new_orderamount): self + { + $this->orderamount = $new_orderamount; + + return $this; + } } diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index dfe449d1..ed38e2f2 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -202,6 +202,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'min' => 0, ]); + $builder->add('orderAmount', NumberConstraintType::class, [ + 'label' => 'part.edit.orderstock', + 'min' => 0, + ]); + + $builder->add('orderDelivery', DateTimeConstraintType::class, [ + 'label' => 'part.edit.orderDelivery', + 'input_type' => DateType::class, + ]); + $builder->add('lotCount', NumberConstraintType::class, [ 'label' => 'part.filter.lot_count', 'min' => 0, diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 4a71f03a..1f7d274b 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -44,6 +44,7 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\ResetType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -95,6 +96,21 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'part.edit.mininstock', 'measurement_unit' => $part->getPartUnit(), ]) + ->add('orderAmount', SIUnitType::class, [ + 'attr' => [ + 'min' => 0, + 'placeholder' => 'part.editmininstock.placeholder', + ], + 'label' => 'part.edit.orderstock', + 'measurement_unit' => $part->getPartUnit(), + ]) + ->add('orderDelivery', DateType::class, [ + 'label' => 'part.edit.orderDelivery', + 'attr' => [], + 'widget' => 'single_text', + 'model_timezone' => 'UTC', + 'required' => false, + ]) ->add('category', StructuralEntityType::class, [ 'class' => Category::class, 'allow_add' => $this->security->isGranted('@categories.create'), diff --git a/src/Serializer/PartNormalizer.php b/src/Serializer/PartNormalizer.php index 650b0214..1e80d5c5 100644 --- a/src/Serializer/PartNormalizer.php +++ b/src/Serializer/PartNormalizer.php @@ -129,6 +129,9 @@ public function denormalize($data, string $type, string $format = null, array $c if (empty($data['minamount'])) { $data['minamount'] = 0.0; } + if (empty($data['orderamount'])) { + $data['orderamount'] = 0.0; + } $context[self::ALREADY_CALLED] = true; diff --git a/src/Services/LabelSystem/SandboxedTwigFactory.php b/src/Services/LabelSystem/SandboxedTwigFactory.php index cdf0594f..4b942356 100644 --- a/src/Services/LabelSystem/SandboxedTwigFactory.php +++ b/src/Services/LabelSystem/SandboxedTwigFactory.php @@ -133,7 +133,7 @@ final class SandboxedTwigFactory Supplier::class => ['getShippingCosts', 'getDefaultCurrency'], Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference', 'getDescription', 'getComment', 'isFavorite', 'getCategory', 'getFootprint', - 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', + 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getOrderAmount', 'getOrderDelivery', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', 'getManufacturerProductUrl', 'getCustomProductURL', 'getManufacturingStatus', 'getManufacturer', 'getManufacturerProductNumber', 'getOrderdetails', 'isObsolete', 'getParameters', 'getGroupedParameters', diff --git a/templates/parts/edit/_main.html.twig b/templates/parts/edit/_main.html.twig index f153d878..8fcdfe86 100644 --- a/templates/parts/edit/_main.html.twig +++ b/templates/parts/edit/_main.html.twig @@ -10,6 +10,8 @@ {{ form_row(form.category) }} {{ form_row(form.tags) }} {{ form_row(form.minAmount) }} +{{ form_row(form.orderAmount) }} +{{ form_row(form.orderDelivery) }} {{ form_row(form.footprint) }} diff --git a/templates/parts/info/_main_infos.html.twig b/templates/parts/info/_main_infos.html.twig index 7bcdd355..df07e401 100644 --- a/templates/parts/info/_main_infos.html.twig +++ b/templates/parts/info/_main_infos.html.twig @@ -55,6 +55,17 @@ {% if part.expiredAmountSum > 0 %} (+{{ part.expiredAmountSum }}) {% endif %} + {% if part.orderAmount > 0 %} + (+ + {{ part.orderAmount | format_amount(part.partUnit) }} + {% if part.orderDelivery %} + @ + + {{ part.orderDelivery | format_date() }}
+
+ {% endif %} + ) + {% endif %} / {{ part.minAmount | format_amount(part.partUnit) }} @@ -93,4 +104,4 @@ {% endif %} #} - \ No newline at end of file + diff --git a/templates/parts/lists/_filter.html.twig b/templates/parts/lists/_filter.html.twig index c29e8ecd..f578f0df 100644 --- a/templates/parts/lists/_filter.html.twig +++ b/templates/parts/lists/_filter.html.twig @@ -66,6 +66,8 @@
{{ form_row(filterForm.storelocation) }} {{ form_row(filterForm.minAmount) }} + {{ form_row(filterForm.orderAmount) }} + {{ form_row(filterForm.orderDelivery) }} {{ form_row(filterForm.amountSum) }} {{ form_row(filterForm.lessThanDesired) }} {{ form_row(filterForm.lotCount) }} @@ -150,4 +152,4 @@ {{ form_end(filterForm) }}
- \ No newline at end of file +