Add unit price and extended price columns to project BOM table

Adds two optional columns to the project BOM datatable (hidden by
default, toggleable via column visibility):

- **Price**: unit price for the BOM entry in the base currency,
  looked up via PricedetailHelper. For parts whose BOM quantity falls
  below the minimum order amount the minimum order amount is used for
  the price tier lookup so that a price is always returned.
- **Extended Price**: unit price multiplied by the BOM quantity.

Prices are rendered via MoneyFormatter (locale-aware, with currency
symbol). Both columns round up to 2 decimal places to avoid displaying
0.00 for very small prices.
This commit is contained in:
MayNiklas 2026-04-15 11:01:12 +02:00
parent 5b86d6f652
commit 1611b6cd41

View file

@ -29,12 +29,16 @@ use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Doctrine\Helpers\FieldHelper;
use App\Entity\Parts\Part;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\Part;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Services\ElementTypeNameGenerator;
use App\Services\EntityURLGenerator;
use App\Services\Formatters\AmountFormatter;
use App\Services\Formatters\MoneyFormatter;
use App\Services\Parts\PricedetailHelper;
use Brick\Math\BigDecimal;
use Brick\Math\RoundingMode;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
@ -50,7 +54,9 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
protected EntityURLGenerator $entityURLGenerator,
protected TranslatorInterface $translator,
protected AmountFormatter $amountFormatter,
protected PartDataTableHelper $partDataTableHelper
protected PartDataTableHelper $partDataTableHelper,
protected PricedetailHelper $pricedetailHelper,
protected MoneyFormatter $moneyFormatter,
) {
}
@ -202,6 +208,27 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
return '';
}
])
->add('price', TextColumn::class, [
'label' => 'project.bom.price',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
$price = $this->getBomEntryUnitPrice($context);
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::UP)->toFloat(), null, 2, true);
},
])
->add('ext_price', TextColumn::class, [
'label' => 'project.bom.ext_price',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
$price = $this->getBomEntryUnitPrice($context);
return $this->moneyFormatter->format(
$price->multipliedBy($context->getQuantity())->toScale(2, RoundingMode::UP)->toFloat(),
null,
2,
true
);
},
])
->add('addedDate', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.addedDate'),
@ -231,6 +258,21 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
]);
}
private function getBomEntryUnitPrice(ProjectBOMEntry $entry): BigDecimal
{
if ($entry->getPart() instanceof Part) {
$amount = $entry->getQuantity();
// If the BOM quantity is below the minimum order amount, use the minimum order amount
// for the price lookup — otherwise calculateAvgPrice returns null (no price tier matches).
$minOrderAmount = $this->pricedetailHelper->getMinOrderAmount($entry->getPart());
if ($minOrderAmount !== null) {
$amount = max($amount, $minOrderAmount);
}
return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $amount) ?? BigDecimal::zero();
}
return $entry->getPrice() ?? BigDecimal::zero();
}
private function getFilterQuery(QueryBuilder $builder, array $options): void
{
$builder