Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.13.0
2.13.1
48 changes: 48 additions & 0 deletions migrations/Version20240905085300.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;

final class Version20240905085300 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Added order fields';
}

public function mySQLUp(Schema $schema): void
{
$this->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 postgreSQLUp(Schema $schema): void
{
$this->addSql('ALTER TABLE parts ADD orderamount DOUBLE PRECISION NOT NULL DEFAULT 0, ADD orderDelivery timestamp');
}

public function postgreSQLDown(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
}
}
21 changes: 21 additions & 0 deletions src/Controller/PartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,27 @@ public function markBulkImportComplete(Part $part, int $jobId, Request $request)
return $this->redirectToRoute('bulk_info_provider_step2', ['jobId' => $jobId]);
}

#[Route(path: '/{id}/delivered', name: 'part_delivered')]
public function delivered(Part $part, Request $request): Response
{
$this->denyAccessUnlessGranted('edit', $part);

$partLot = $part->getPartLots()[0] ?? null;
if (!$partLot instanceof PartLot) {
$this->addFlash('error', 'part.delivered.error.no_lot');
return $this->redirectToRoute('part_info', ['id' => $part->getID()]);
}

$partLot->setAmount($partLot->getAmount() + $part->getOrderAmount());
$part->setOrderAmount(0);
$part->setOrderDelivery(null);

$this->em->persist($part);
$this->em->flush();

return $this->redirectToRoute('part_info', ['id' => $part->getID()]);
}

#[Route(path: '/{id}/delete', name: 'part_delete', methods: ['DELETE'])]
public function delete(Request $request, Part $part): RedirectResponse
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class LessThanDesiredConstraint extends BooleanConstraint
public function __construct(?string $property = null, ?string $identifier = null, ?bool $default_value = null)
{
parent::__construct($property ?? '(
SELECT COALESCE(SUM(ld_partLot.amount), 0.0)
SELECT COALESCE(SUM(ld_partLot.amount) + part.orderamount, 0.0)
FROM '.PartLot::class.' ld_partLot
WHERE ld_partLot.part = part.id
AND ld_partLot.instock_unknown = false
Expand All @@ -48,7 +48,7 @@ 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.minamount');
} else {
$queryBuilder->andHaving($this->property . ' >= part.minamount');
}
Expand Down
4 changes: 4 additions & 0 deletions src/DataTables/Filters/PartFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,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;
Expand Down Expand Up @@ -140,6 +142,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
Expand Down
9 changes: 9 additions & 0 deletions src/DataTables/PartsDataTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ public function configure(DataTable $dataTable, array $options): void
$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)',
Expand Down
76 changes: 74 additions & 2 deletions src/DataTables/ProjectBomEntriesDataTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use App\DataTables\Column\HTMLColumn;
use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Column\TagsColumn;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Doctrine\Helpers\FieldHelper;
use App\Entity\Parts\ManufacturingStatus;
Expand Down Expand Up @@ -102,7 +103,7 @@ public function configure(DataTable $dataTable, array $options): void
])
->add('partId', TextColumn::class, [
'label' => $this->translator->trans('project.bom.part_id'),
'visible' => true,
'visible' => false,
'orderField' => 'part.id',
'data' => function (ProjectBOMEntry $context) {
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
Expand Down Expand Up @@ -150,6 +151,7 @@ public function configure(DataTable $dataTable, array $options): void
])
->add('footprint', EntityColumn::class, [
'property' => 'part.footprint',
'visible' => false,
'label' => $this->translator->trans('part.table.footprint'),
'orderField' => 'NATSORT(footprint.name)'
])
Expand All @@ -159,6 +161,39 @@ public function configure(DataTable $dataTable, array $options): void
'label' => $this->translator->trans('part.table.manufacturer'),
'orderField' => 'NATSORT(manufacturer.name)'
])
->add('supplier', HTMLColumn::class, [
'label' => $this->translator->trans('supplier.label'),
'visible' => true,
// Use an aggregate because a part can have multiple supplier orderdetails.
'orderField' => 'NATSORT(MIN(_suppliers.name))',
'data' => function (ProjectBOMEntry $context): string {
if (!$context->getPart() instanceof Part) {
return '';
}

$supplierLinks = [];
foreach ($context->getPart()->getOrderdetails(true) as $orderdetail) {
$supplier = $orderdetail->getSupplier();
$supplierName = trim((string) $supplier->getName());
if ($supplierName === '') {
continue;
}

$supplierId = $supplier->getId();
if (isset($supplierLinks[$supplierId])) {
continue;
}

$supplierLinks[$supplierId] = sprintf(
'<a href="%s">%s</a>',
htmlspecialchars($this->entityURLGenerator->listPartsURL($supplier)),
htmlspecialchars($supplierName)
);
}

return implode(', ', $supplierLinks);
},
])

->add('manufacturing_status', EnumColumn::class, [
'label' => $this->translator->trans('part.table.manufacturingStatus'),
Expand All @@ -173,9 +208,14 @@ public function configure(DataTable $dataTable, array $options): void
return $this->translator->trans($status->toTranslationKey());
},
])
->add('tags', TagsColumn::class, [
'label' => $this->translator->trans('part.table.tags'),
'data' => static fn (ProjectBOMEntry $context): string => $context->getPart()?->getTags() ?? '',
])

->add('mountnames', HTMLColumn::class, [
'label' => 'project.bom.mountnames',
'visible' => false,
'data' => function (ProjectBOMEntry $context) {
$html = '';

Expand All @@ -188,7 +228,7 @@ public function configure(DataTable $dataTable, array $options): void

->add('instockAmount', HTMLColumn::class, [
'label' => 'project.bom.instockAmount',
'visible' => false,
'visible' => true,
'data' => function (ProjectBOMEntry $context) {
if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderAmount($context->getPart());
Expand All @@ -197,6 +237,30 @@ public function configure(DataTable $dataTable, array $options): void
return '';
},
])
->add('minAmount', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.minamount'),
'visible' => true,
'orderField' => 'part.minamount',
'data' => function (ProjectBOMEntry $context): string {
if (!$context->getPart() instanceof Part) {
return '';
}

return $this->amountFormatter->format($context->getPart()->getMinAmount(), $context->getPart()->getPartUnit());
},
])
->add('orderAmount', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.orderamount'),
'visible' => true,
'orderField' => 'part.orderamount',
'data' => function (ProjectBOMEntry $context): string {
if (!$context->getPart() instanceof Part) {
return '';
}

return $this->amountFormatter->format($context->getPart()->getOrderAmount(), $context->getPart()->getPartUnit());
},
])
->add('storelocation', HTMLColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'),
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
Expand Down Expand Up @@ -272,6 +336,8 @@ private function getFilterQuery(QueryBuilder $builder, array $options): void
->leftJoin('_partLots.storage_location', '_storelocations')
->leftJoin('part.footprint', 'footprint')
->leftJoin('part.manufacturer', 'manufacturer')
->leftJoin('part.orderdetails', '_orderdetails')
->leftJoin('_orderdetails.supplier', '_suppliers')
->leftJoin('part.partCustomState', 'partCustomState')
->where('bom_entry.project = :project')
->setParameter('project', $options['project'])
Expand Down Expand Up @@ -299,6 +365,8 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v
->addSelect('storelocations')
->addSelect('footprint')
->addSelect('manufacturer')
->addSelect('orderdetails')
->addSelect('suppliers')
->addSelect('partCustomState')
->from(ProjectBOMEntry::class, 'bom_entry')
->leftJoin('bom_entry.part', 'part')
Expand All @@ -307,6 +375,8 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v
->leftJoin('partLots.storage_location', 'storelocations')
->leftJoin('part.footprint', 'footprint')
->leftJoin('part.manufacturer', 'manufacturer')
->leftJoin('part.orderdetails', 'orderdetails')
->leftJoin('orderdetails.supplier', 'suppliers')
->leftJoin('part.partCustomState', 'partCustomState')
->where('bom_entry.id IN (:ids)')
->setParameter('ids', $ids)
Expand All @@ -317,6 +387,8 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v
->addGroupBy('storelocations')
->addGroupBy('footprint')
->addGroupBy('manufacturer')
->addGroupBy('orderdetails')
->addGroupBy('suppliers')
->addGroupBy('partCustomState')

->setHint(Query::HINT_READ_ONLY, true)
Expand Down
5 changes: 3 additions & 2 deletions src/Entity/Parts/Part.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\DBAL\Types\Types;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
Expand Down Expand Up @@ -111,9 +112,9 @@
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])]
#[ApiFilter(TagFilter::class, properties: ["tags"])]
#[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;
Expand Down
31 changes: 31 additions & 0 deletions src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
namespace App\Entity\Parts\PartTraits;

use App\Entity\Parts\InfoProviderReference;
use App\Validator\Constraints\Year2038BugWorkaround;
use App\Entity\Parts\PartCustomState;
use App\Validator\Constraints\ValidGTIN;
use Doctrine\DBAL\Types\Types;
Expand Down Expand Up @@ -60,6 +61,15 @@ trait AdvancedPropertyTrait
#[ORM\Column(type: Types::FLOAT, nullable: true)]
protected ?float $mass = null;

/**
* @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(type: Types::DATETIME_MUTABLE, nullable: true)]
#[Year2038BugWorkaround]
protected ?\DateTimeInterface $orderDelivery = null;

/**
* @var string|null The internal part number of the part
*/
Expand Down Expand Up @@ -162,6 +172,27 @@ public function setMass(?float $mass): self
return $this;
}

/**
* 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.
*
* @param \DateTimeInterface|null $orderDelivery The new delivery date
*
* @return $this
*/
public function setOrderDelivery(?\DateTimeInterface $orderDelivery): self
{
$this->orderDelivery = $orderDelivery;
return $this;
}

/**
* Returns the internal part number of the part.
* @return string
Expand Down
Loading