diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index 6ee726e8..2d5c4ebc 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -36,8 +36,7 @@ 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 App\Services\ProjectSystem\ProjectBuildHelper; use Brick\Math\RoundingMode; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\Query; @@ -55,7 +54,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface protected TranslatorInterface $translator, protected AmountFormatter $amountFormatter, protected PartDataTableHelper $partDataTableHelper, - protected PricedetailHelper $pricedetailHelper, + protected ProjectBuildHelper $projectBuildHelper, protected MoneyFormatter $moneyFormatter, ) { } @@ -212,7 +211,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface 'label' => 'project.bom.price', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { - $price = $this->getBomEntryUnitPrice($context); + $price = $this->projectBuildHelper->getEntryUnitPrice($context); return $this->moneyFormatter->format($price->toScale(2, RoundingMode::UP)->toFloat(), null, 2, true); }, ]) @@ -220,7 +219,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface 'label' => 'project.bom.ext_price', 'visible' => false, 'render' => function ($value, ProjectBOMEntry $context) { - $price = $this->getBomEntryUnitPrice($context); + $price = $this->projectBuildHelper->getEntryUnitPrice($context); return $this->moneyFormatter->format( $price->multipliedBy($context->getQuantity())->toScale(2, RoundingMode::UP)->toFloat(), null, @@ -258,21 +257,6 @@ 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 diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php index 9d643a87..ee5b8c68 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -227,6 +227,16 @@ final readonly class ProjectBuildHelper ?->toScale(2, RoundingMode::UP); } + /** + * Returns the effective unit price for a single piece of the given BOM entry, + * taking bulk pricing and minimum order amounts into account for N builds. + * Returns BigDecimal::zero() when no pricing data is available. + */ + public function getEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds = 1, ?Currency $currency = null): BigDecimal + { + return $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency) ?? BigDecimal::zero(); + } + /** * Returns the effective unit price for a single piece of the given BOM entry, * taking bulk pricing into account for N builds. diff --git a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php index cf36b030..de9f9406 100644 --- a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php +++ b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php @@ -264,6 +264,37 @@ final class ProjectBuildHelperTest extends WebTestCase $this->assertTrue(BigDecimal::of('11.00')->isEqualTo($result)); } + public function testGetEntryUnitPriceReturnsZeroForNoPricingData(): void + { + $entry = new ProjectBOMEntry(); + $entry->setPart(new Part()); // part with no orderdetails + $entry->setQuantity(5); + + $result = $this->service->getEntryUnitPrice($entry); + $this->assertTrue(BigDecimal::zero()->isEqualTo($result)); + } + + public function testGetEntryUnitPriceNonPartEntry(): void + { + $entry = new ProjectBOMEntry(); + $entry->setName('Wire'); + $entry->setQuantity(2); + $entry->setPrice(BigDecimal::of('1.25')); + + $result = $this->service->getEntryUnitPrice($entry); + $this->assertTrue(BigDecimal::of('1.25')->isEqualTo($result)); + } + + public function testGetEntryUnitPriceWithPart(): void + { + $entry = new ProjectBOMEntry(); + $entry->setPart($this->makePartWithPrice(2.00)); + $entry->setQuantity(3); + + $result = $this->service->getEntryUnitPrice($entry); + $this->assertTrue(BigDecimal::of('2.00')->isEqualTo($result)); + } + public function testCalculateTotalBuildPriceRespectsMinOrderAmount(): void { $project = new Project();