Part-DB-server/src/DataTables/ProjectBomEntriesDataTable.php

332 lines
14 KiB
PHP
Raw Normal View History

2022-12-18 21:58:21 +01:00
<?php
/**
2022-12-18 21:58:21 +01:00
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
2022-12-18 21:58:21 +01:00
namespace App\DataTables;
use App\DataTables\Adapters\TwoStepORMAdapter;
2022-12-18 21:58:21 +01:00
use App\DataTables\Column\EntityColumn;
use App\DataTables\Column\EnumColumn;
2022-12-18 21:58:21 +01:00
use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Doctrine\Helpers\FieldHelper;
use App\Entity\Parts\ManufacturingStatus;
Add price columns to project BOM table and build price summary (#1345) * 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. * Add translation key for project.bom.ext_price Adds the English translation "Extended Price" for the new BOM extended price column. Other languages are marked needs-translation and will be picked up by Crowdin. * Add build price summary to project info tab Displays the total BOM price for N builds on the project info page, using the existing price-tier logic from PricedetailHelper. The user can adjust the number of builds via a small form; the unit price is also shown when N > 1. New backend: - ProjectBuildHelper gains calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and roundedUnitBuildPrice() — bulk-order quantities are factored in so that price tiers apply correctly across N builds. - ProjectController::info() now reads ?n= and passes number_of_builds to the template. Template (_info.html.twig): - Adds price badge (hidden when no pricing data is available). - Adds number-of-builds form that reloads the info page. * Add tests for build price calculation in ProjectBuildHelper Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and the private getBomEntryUnitPrice() helper. Scenarios tested: empty project, no pricing data, non-part BOM entries with manual prices, part entries with pricedetails, mixed entries, rounding-up of sub-cent prices, and minimum order amount floor for price tier lookup. * Deduplicate BOM entry price logic into ProjectBuildHelper The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was identical to the one in ProjectBuildHelper. Replaced it with a new public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal, never null) and delegate to it from the DataTable. This eliminates the duplicate code and brings the DataTable lines under the existing ProjectBuildHelper test coverage. Added three tests for getEntryUnitPrice() covering the no-pricing, non-part, and part cases. * Added type hint to service --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-04-15 22:13:07 +02:00
use App\Entity\Parts\Part;
2022-12-18 21:58:21 +01:00
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Services\ElementTypeNameGenerator;
2022-12-18 21:58:21 +01:00
use App\Services\EntityURLGenerator;
use App\Services\Formatters\AmountFormatter;
Add price columns to project BOM table and build price summary (#1345) * 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. * Add translation key for project.bom.ext_price Adds the English translation "Extended Price" for the new BOM extended price column. Other languages are marked needs-translation and will be picked up by Crowdin. * Add build price summary to project info tab Displays the total BOM price for N builds on the project info page, using the existing price-tier logic from PricedetailHelper. The user can adjust the number of builds via a small form; the unit price is also shown when N > 1. New backend: - ProjectBuildHelper gains calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and roundedUnitBuildPrice() — bulk-order quantities are factored in so that price tiers apply correctly across N builds. - ProjectController::info() now reads ?n= and passes number_of_builds to the template. Template (_info.html.twig): - Adds price badge (hidden when no pricing data is available). - Adds number-of-builds form that reloads the info page. * Add tests for build price calculation in ProjectBuildHelper Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and the private getBomEntryUnitPrice() helper. Scenarios tested: empty project, no pricing data, non-part BOM entries with manual prices, part entries with pricedetails, mixed entries, rounding-up of sub-cent prices, and minimum order amount floor for price tier lookup. * Deduplicate BOM entry price logic into ProjectBuildHelper The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was identical to the one in ProjectBuildHelper. Replaced it with a new public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal, never null) and delegate to it from the DataTable. This eliminates the duplicate code and brings the DataTable lines under the existing ProjectBuildHelper test coverage. Added three tests for getEntryUnitPrice() covering the no-pricing, non-part, and part cases. * Added type hint to service --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-04-15 22:13:07 +02:00
use App\Services\Formatters\MoneyFormatter;
use App\Services\ProjectSystem\ProjectBuildHelper;
2026-05-19 21:17:45 +02:00
use Brick\Math\BigDecimal;
Add price columns to project BOM table and build price summary (#1345) * 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. * Add translation key for project.bom.ext_price Adds the English translation "Extended Price" for the new BOM extended price column. Other languages are marked needs-translation and will be picked up by Crowdin. * Add build price summary to project info tab Displays the total BOM price for N builds on the project info page, using the existing price-tier logic from PricedetailHelper. The user can adjust the number of builds via a small form; the unit price is also shown when N > 1. New backend: - ProjectBuildHelper gains calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and roundedUnitBuildPrice() — bulk-order quantities are factored in so that price tiers apply correctly across N builds. - ProjectController::info() now reads ?n= and passes number_of_builds to the template. Template (_info.html.twig): - Adds price badge (hidden when no pricing data is available). - Adds number-of-builds form that reloads the info page. * Add tests for build price calculation in ProjectBuildHelper Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and the private getBomEntryUnitPrice() helper. Scenarios tested: empty project, no pricing data, non-part BOM entries with manual prices, part entries with pricedetails, mixed entries, rounding-up of sub-cent prices, and minimum order amount floor for price tier lookup. * Deduplicate BOM entry price logic into ProjectBuildHelper The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was identical to the one in ProjectBuildHelper. Replaced it with a new public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal, never null) and delegate to it from the DataTable. This eliminates the duplicate code and brings the DataTable lines under the existing ProjectBuildHelper test coverage. Added three tests for getEntryUnitPrice() covering the no-pricing, non-part, and part cases. * Added type hint to service --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-04-15 22:13:07 +02:00
use Brick\Math\RoundingMode;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query;
2022-12-18 21:58:21 +01:00
use Doctrine\ORM\QueryBuilder;
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
use Omines\DataTablesBundle\Column\TextColumn;
use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ProjectBomEntriesDataTable implements DataTableTypeInterface
{
public function __construct(
protected EntityURLGenerator $entityURLGenerator,
protected TranslatorInterface $translator,
protected AmountFormatter $amountFormatter,
Add price columns to project BOM table and build price summary (#1345) * 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. * Add translation key for project.bom.ext_price Adds the English translation "Extended Price" for the new BOM extended price column. Other languages are marked needs-translation and will be picked up by Crowdin. * Add build price summary to project info tab Displays the total BOM price for N builds on the project info page, using the existing price-tier logic from PricedetailHelper. The user can adjust the number of builds via a small form; the unit price is also shown when N > 1. New backend: - ProjectBuildHelper gains calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and roundedUnitBuildPrice() — bulk-order quantities are factored in so that price tiers apply correctly across N builds. - ProjectController::info() now reads ?n= and passes number_of_builds to the template. Template (_info.html.twig): - Adds price badge (hidden when no pricing data is available). - Adds number-of-builds form that reloads the info page. * Add tests for build price calculation in ProjectBuildHelper Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and the private getBomEntryUnitPrice() helper. Scenarios tested: empty project, no pricing data, non-part BOM entries with manual prices, part entries with pricedetails, mixed entries, rounding-up of sub-cent prices, and minimum order amount floor for price tier lookup. * Deduplicate BOM entry price logic into ProjectBuildHelper The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was identical to the one in ProjectBuildHelper. Replaced it with a new public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal, never null) and delegate to it from the DataTable. This eliminates the duplicate code and brings the DataTable lines under the existing ProjectBuildHelper test coverage. Added three tests for getEntryUnitPrice() covering the no-pricing, non-part, and part cases. * Added type hint to service --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-04-15 22:13:07 +02:00
protected PartDataTableHelper $partDataTableHelper,
protected ProjectBuildHelper $projectBuildHelper,
protected MoneyFormatter $moneyFormatter,
) {
2022-12-18 21:58:21 +01:00
}
public function configure(DataTable $dataTable, array $options): void
2022-12-18 21:58:21 +01:00
{
$dataTable
//->add('select', SelectColumn::class)
->add('picture', TextColumn::class, [
'label' => '',
'className' => 'no-colvis',
'render' => function ($value, ProjectBOMEntry $context) {
2023-06-11 14:55:06 +02:00
if(!$context->getPart() instanceof Part) {
2022-12-18 21:58:21 +01:00
return '';
}
return $this->partDataTableHelper->renderPicture($context->getPart());
}
2022-12-18 21:58:21 +01:00
])
->add('id', TextColumn::class, [
'label' => $this->translator->trans('part.table.id'),
'visible' => false,
])
->add('quantity', TextColumn::class, [
'label' => $this->translator->trans('project.bom.quantity'),
2022-12-18 21:58:21 +01:00
'className' => 'text-center',
'orderField' => 'bom_entry.quantity',
'render' => function ($value, ProjectBOMEntry $context): float|string {
2022-12-18 21:58:21 +01:00
//If we have a non-part entry, only show the rounded quantity
2023-06-11 14:55:06 +02:00
if (!$context->getPart() instanceof Part) {
2022-12-18 21:58:21 +01:00
return round($context->getQuantity());
}
//Otherwise use the unit of the part to format the quantity
return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()));
2022-12-18 21:58:21 +01:00
},
])
2026-05-19 21:17:45 +02:00
->add('partId', TextColumn::class, [
'label' => $this->translator->trans('project.bom.part_id'),
'visible' => true,
'orderField' => 'part.id',
'render' => function ($value, ProjectBOMEntry $context) {
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
},
])
2022-12-18 21:58:21 +01:00
->add('name', TextColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'orderField' => 'NATSORT(part.name)',
2022-12-18 21:58:21 +01:00
'render' => function ($value, ProjectBOMEntry $context) {
2023-06-11 14:55:06 +02:00
if(!$context->getPart() instanceof Part) {
2024-06-22 00:31:43 +02:00
return htmlspecialchars((string) $context->getName());
2022-12-18 21:58:21 +01:00
}
2023-06-13 20:24:54 +02:00
//Part exists if we reach this point
$tmp = $this->partDataTableHelper->renderName($context->getPart());
if($context->getName() !== null && $context->getName() !== '') {
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
}
return $tmp;
2022-12-18 21:58:21 +01:00
},
])
->add('ipn', TextColumn::class, [
'label' => $this->translator->trans('part.table.ipn'),
'orderField' => 'NATSORT(part.ipn)',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if($context->getPart() instanceof Part) {
return $context->getPart()->getIpn();
}
}
])
2022-12-18 21:58:21 +01:00
->add('description', MarkdownColumn::class, [
'label' => $this->translator->trans('part.table.description'),
'data' => function (ProjectBOMEntry $context) {
2023-06-11 14:55:06 +02:00
if($context->getPart() instanceof Part) {
2022-12-18 21:58:21 +01:00
return $context->getPart()->getDescription();
}
//For non-part BOM entries show the comment field
return $context->getComment();
},
])
->add('category', EntityColumn::class, [
'label' => $this->translator->trans('part.table.category'),
'property' => 'part.category',
'orderField' => 'NATSORT(category.name)'
2022-12-18 21:58:21 +01:00
])
->add('footprint', EntityColumn::class, [
'property' => 'part.footprint',
'label' => $this->translator->trans('part.table.footprint'),
'orderField' => 'NATSORT(footprint.name)'
2022-12-18 21:58:21 +01:00
])
->add('manufacturer', EntityColumn::class, [
'property' => 'part.manufacturer',
'label' => $this->translator->trans('part.table.manufacturer'),
'orderField' => 'NATSORT(manufacturer.name)'
2022-12-18 21:58:21 +01:00
])
->add('manufacturing_status', EnumColumn::class, [
'label' => $this->translator->trans('part.table.manufacturingStatus'),
'data' => static fn(ProjectBOMEntry $context): ?ManufacturingStatus => $context->getPart()?->getManufacturingStatus(),
'orderField' => 'part.manufacturing_status',
2026-05-19 21:17:45 +02:00
'class' => ManufacturingStatus::class,
'render' => function (?ManufacturingStatus $status, ProjectBOMEntry $context): string {
if ($status === null) {
return '';
}
return $this->translator->trans($status->toTranslationKey());
},
])
2022-12-18 21:58:21 +01:00
->add('mountnames', TextColumn::class, [
'label' => 'project.bom.mountnames',
'render' => function ($value, ProjectBOMEntry $context) {
$html = '';
2022-12-18 21:58:21 +01:00
foreach (explode(',', $context->getMountnames()) as $mountname) {
$html .= sprintf('<span class="badge badge-secondary bg-secondary">%s</span> ', htmlspecialchars($mountname));
}
return $html;
},
2022-12-18 21:58:21 +01:00
])
->add('instockAmount', TextColumn::class, [
'label' => 'project.bom.instockAmount',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
2024-06-22 00:31:43 +02:00
if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderAmount($context->getPart());
}
return '';
}
])
->add('storelocation', TextColumn::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
'orderField' => 'NATSORT(MIN(_storelocations.name))',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
2024-06-22 00:31:43 +02:00
if ($context->getPart() !== null) {
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
}
return '';
}
])
Add price columns to project BOM table and build price summary (#1345) * 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. * Add translation key for project.bom.ext_price Adds the English translation "Extended Price" for the new BOM extended price column. Other languages are marked needs-translation and will be picked up by Crowdin. * Add build price summary to project info tab Displays the total BOM price for N builds on the project info page, using the existing price-tier logic from PricedetailHelper. The user can adjust the number of builds via a small form; the unit price is also shown when N > 1. New backend: - ProjectBuildHelper gains calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and roundedUnitBuildPrice() — bulk-order quantities are factored in so that price tiers apply correctly across N builds. - ProjectController::info() now reads ?n= and passes number_of_builds to the template. Template (_info.html.twig): - Adds price badge (hidden when no pricing data is available). - Adds number-of-builds form that reloads the info page. * Add tests for build price calculation in ProjectBuildHelper Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and the private getBomEntryUnitPrice() helper. Scenarios tested: empty project, no pricing data, non-part BOM entries with manual prices, part entries with pricedetails, mixed entries, rounding-up of sub-cent prices, and minimum order amount floor for price tier lookup. * Deduplicate BOM entry price logic into ProjectBuildHelper The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was identical to the one in ProjectBuildHelper. Replaced it with a new public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal, never null) and delegate to it from the DataTable. This eliminates the duplicate code and brings the DataTable lines under the existing ProjectBuildHelper test coverage. Added three tests for getEntryUnitPrice() covering the no-pricing, non-part, and part cases. * Added type hint to service --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-04-15 22:13:07 +02:00
->add('price', TextColumn::class, [
'label' => 'project.bom.price',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
2026-05-19 21:17:45 +02:00
return $this->moneyFormatter->format($price->toScale(2, RoundingMode::Up)->toFloat(), null, 2, true);
Add price columns to project BOM table and build price summary (#1345) * 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. * Add translation key for project.bom.ext_price Adds the English translation "Extended Price" for the new BOM extended price column. Other languages are marked needs-translation and will be picked up by Crowdin. * Add build price summary to project info tab Displays the total BOM price for N builds on the project info page, using the existing price-tier logic from PricedetailHelper. The user can adjust the number of builds via a small form; the unit price is also shown when N > 1. New backend: - ProjectBuildHelper gains calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and roundedUnitBuildPrice() — bulk-order quantities are factored in so that price tiers apply correctly across N builds. - ProjectController::info() now reads ?n= and passes number_of_builds to the template. Template (_info.html.twig): - Adds price badge (hidden when no pricing data is available). - Adds number-of-builds form that reloads the info page. * Add tests for build price calculation in ProjectBuildHelper Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and the private getBomEntryUnitPrice() helper. Scenarios tested: empty project, no pricing data, non-part BOM entries with manual prices, part entries with pricedetails, mixed entries, rounding-up of sub-cent prices, and minimum order amount floor for price tier lookup. * Deduplicate BOM entry price logic into ProjectBuildHelper The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was identical to the one in ProjectBuildHelper. Replaced it with a new public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal, never null) and delegate to it from the DataTable. This eliminates the duplicate code and brings the DataTable lines under the existing ProjectBuildHelper test coverage. Added three tests for getEntryUnitPrice() covering the no-pricing, non-part, and part cases. * Added type hint to service --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-04-15 22:13:07 +02:00
},
])
->add('ext_price', TextColumn::class, [
'label' => 'project.bom.ext_price',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
$price = $this->projectBuildHelper->getEntryUnitPrice($context);
return $this->moneyFormatter->format(
2026-05-19 21:17:45 +02:00
$price->multipliedBy(BigDecimal::fromFloatShortest($context->getQuantity()))
->toScale(2, RoundingMode::Up)->toFloat(),
Add price columns to project BOM table and build price summary (#1345) * 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. * Add translation key for project.bom.ext_price Adds the English translation "Extended Price" for the new BOM extended price column. Other languages are marked needs-translation and will be picked up by Crowdin. * Add build price summary to project info tab Displays the total BOM price for N builds on the project info page, using the existing price-tier logic from PricedetailHelper. The user can adjust the number of builds via a small form; the unit price is also shown when N > 1. New backend: - ProjectBuildHelper gains calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and roundedUnitBuildPrice() — bulk-order quantities are factored in so that price tiers apply correctly across N builds. - ProjectController::info() now reads ?n= and passes number_of_builds to the template. Template (_info.html.twig): - Adds price badge (hidden when no pricing data is available). - Adds number-of-builds form that reloads the info page. * Add tests for build price calculation in ProjectBuildHelper Covers calculateTotalBuildPrice(), calculateUnitBuildPrice(), roundedTotalBuildPrice(), and the private getBomEntryUnitPrice() helper. Scenarios tested: empty project, no pricing data, non-part BOM entries with manual prices, part entries with pricedetails, mixed entries, rounding-up of sub-cent prices, and minimum order amount floor for price tier lookup. * Deduplicate BOM entry price logic into ProjectBuildHelper The private getBomEntryUnitPrice() in ProjectBomEntriesDataTable was identical to the one in ProjectBuildHelper. Replaced it with a new public getEntryUnitPrice() on ProjectBuildHelper (returns BigDecimal, never null) and delegate to it from the DataTable. This eliminates the duplicate code and brings the DataTable lines under the existing ProjectBuildHelper test coverage. Added three tests for getEntryUnitPrice() covering the no-pricing, non-part, and part cases. * Added type hint to service --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-04-15 22:13:07 +02:00
null,
2,
true
);
},
])
2022-12-18 21:58:21 +01:00
->add('addedDate', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.addedDate'),
'visible' => false,
])
->add('lastModified', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.lastModified'),
'visible' => false,
])
;
$dataTable->addOrderBy('name', DataTable::SORT_ASCENDING);
$dataTable->createAdapter(TwoStepORMAdapter::class, [
'entity' => ProjectBOMEntry::class,
'hydrate' => AbstractQuery::HYDRATE_OBJECT,
'filter_query' => function (QueryBuilder $builder) use ($options): void {
$this->getFilterQuery($builder, $options);
2022-12-18 21:58:21 +01:00
},
'detail_query' => $this->getDetailQuery(...),
2022-12-18 21:58:21 +01:00
'criteria' => [
function (QueryBuilder $builder) use ($options): void {
$this->buildCriteria($builder, $options);
},
new SearchCriteriaProvider(),
],
]);
}
private function getFilterQuery(QueryBuilder $builder, array $options): void
2022-12-18 21:58:21 +01:00
{
$builder
->select('bom_entry.id')
2022-12-18 21:58:21 +01:00
->from(ProjectBOMEntry::class, 'bom_entry')
->leftJoin('bom_entry.part', 'part')
->leftJoin('part.category', 'category')
->leftJoin('part.partLots', '_partLots')
->leftJoin('_partLots.storage_location', '_storelocations')
->leftJoin('part.footprint', 'footprint')
->leftJoin('part.manufacturer', 'manufacturer')
->leftJoin('part.partCustomState', 'partCustomState')
2022-12-18 21:58:21 +01:00
->where('bom_entry.project = :project')
2023-02-05 03:01:25 +01:00
->setParameter('project', $options['project'])
->addGroupBy('bom_entry')
->addGroupBy('part')
->addGroupBy('category')
->addGroupBy('footprint')
->addGroupBy('manufacturer')
->addGroupBy('partCustomState')
2022-12-18 21:58:21 +01:00
;
}
private function getDetailQuery(QueryBuilder $builder, array $filter_results): void
{
$ids = array_map(static fn (array $row) => $row['id'], $filter_results);
if ($ids === []) {
$ids = [-1];
}
$builder
->select('bom_entry')
->addSelect('part')
->addSelect('category')
->addSelect('partLots')
->addSelect('storelocations')
->addSelect('footprint')
->addSelect('manufacturer')
->addSelect('partCustomState')
->from(ProjectBOMEntry::class, 'bom_entry')
->leftJoin('bom_entry.part', 'part')
->leftJoin('part.category', 'category')
->leftJoin('part.partLots', 'partLots')
->leftJoin('partLots.storage_location', 'storelocations')
->leftJoin('part.footprint', 'footprint')
->leftJoin('part.manufacturer', 'manufacturer')
->leftJoin('part.partCustomState', 'partCustomState')
->where('bom_entry.id IN (:ids)')
->setParameter('ids', $ids)
->addGroupBy('bom_entry')
->addGroupBy('part')
->addGroupBy('partLots')
->addGroupBy('category')
->addGroupBy('storelocations')
->addGroupBy('footprint')
->addGroupBy('manufacturer')
->addGroupBy('partCustomState')
->setHint(Query::HINT_READ_ONLY, true)
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, false)
;
FieldHelper::addOrderByFieldParam($builder, 'bom_entry.id', 'ids');
}
2022-12-18 21:58:21 +01:00
private function buildCriteria(QueryBuilder $builder, array $options): void
{
}
}