mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-18 17:31:35 +00:00
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.
This commit is contained in:
parent
e9fb0dba51
commit
82e55bc83d
4 changed files with 121 additions and 7 deletions
|
|
@ -69,10 +69,13 @@ class ProjectController extends AbstractController
|
|||
return $table->getResponse();
|
||||
}
|
||||
|
||||
$number_of_builds = max(1, $request->query->getInt('n', 1));
|
||||
|
||||
return $this->render('projects/info/info.html.twig', [
|
||||
'buildHelper' => $buildHelper,
|
||||
'datatable' => $table,
|
||||
'project' => $project,
|
||||
'number_of_builds' => $number_of_builds,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,16 +25,22 @@ namespace App\Services\ProjectSystem;
|
|||
use App\Entity\Parts\Part;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Helpers\Projects\ProjectBuildRequest;
|
||||
use App\Services\Parts\PartLotWithdrawAddHelper;
|
||||
use App\Services\Parts\PricedetailHelper;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest
|
||||
*/
|
||||
final readonly class ProjectBuildHelper
|
||||
{
|
||||
public function __construct(private PartLotWithdrawAddHelper $withdraw_add_helper)
|
||||
{
|
||||
public function __construct(
|
||||
private PartLotWithdrawAddHelper $withdraw_add_helper,
|
||||
private PricedetailHelper $pricedetailHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,4 +174,71 @@ final readonly class ProjectBuildHelper
|
|||
$this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total price to build the given project N times, taking bulk pricing into account.
|
||||
* Returns null if no BOM entry has any pricing information.
|
||||
*/
|
||||
public function calculateTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
|
||||
{
|
||||
$total = BigDecimal::zero();
|
||||
$has_price = false;
|
||||
|
||||
foreach ($project->getBomEntries() as $entry) {
|
||||
$unit_price = $this->getBomEntryUnitPrice($entry, $number_of_builds, $currency);
|
||||
if ($unit_price === null) {
|
||||
continue;
|
||||
}
|
||||
$has_price = true;
|
||||
$total = $total->plus($unit_price->multipliedBy($entry->getQuantity())->multipliedBy($number_of_builds));
|
||||
}
|
||||
|
||||
return $has_price ? $total : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the price to build one unit of the given project when ordering for N builds in total.
|
||||
* Returns null if no BOM entry has any pricing information.
|
||||
*/
|
||||
public function calculateUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
|
||||
{
|
||||
$total = $this->calculateTotalBuildPrice($project, $number_of_builds, $currency);
|
||||
if ($total === null) {
|
||||
return null;
|
||||
}
|
||||
return $total->dividedBy($number_of_builds, 10, RoundingMode::HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total build price rounded up to 2 decimal places, ready for display.
|
||||
*/
|
||||
public function roundedTotalBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
|
||||
{
|
||||
return $this->calculateTotalBuildPrice($project, $number_of_builds, $currency)
|
||||
?->toScale(2, RoundingMode::UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unit build price rounded up to 2 decimal places, ready for display.
|
||||
*/
|
||||
public function roundedUnitBuildPrice(Project $project, int $number_of_builds = 1, ?Currency $currency = null): ?BigDecimal
|
||||
{
|
||||
return $this->calculateUnitBuildPrice($project, $number_of_builds, $currency)
|
||||
?->toScale(2, RoundingMode::UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective unit price for a single piece of the given BOM entry,
|
||||
* taking bulk pricing into account for N builds.
|
||||
*/
|
||||
private function getBomEntryUnitPrice(ProjectBOMEntry $entry, int $number_of_builds, ?Currency $currency): ?BigDecimal
|
||||
{
|
||||
if ($entry->getPart() instanceof Part) {
|
||||
$total_qty = $entry->getQuantity() * $number_of_builds;
|
||||
$min_order = $this->pricedetailHelper->getMinOrderAmount($entry->getPart());
|
||||
$effective_qty = ($min_order !== null) ? max($total_qty, $min_order) : $total_qty;
|
||||
return $this->pricedetailHelper->calculateAvgPrice($entry->getPart(), $effective_qty, $currency);
|
||||
}
|
||||
return $entry->getPrice();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,32 @@
|
|||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
{% set n = number_of_builds ?? 1 %}
|
||||
{% set total_build_price = buildHelper.roundedTotalBuildPrice(project, n, app.user.currency ?? null) %}
|
||||
{% set unit_build_price = buildHelper.roundedUnitBuildPrice(project, n, app.user.currency ?? null) %}
|
||||
{% if total_build_price is not null %}
|
||||
<div class="mt-1">
|
||||
<h6>
|
||||
<span class="badge badge-primary bg-success">
|
||||
<i class="fa-solid fa-money-bill-wave fa-fw"></i>
|
||||
{% trans %}project.info.total_build_price{% endtrans %}:
|
||||
{{ total_build_price | format_money(app.user.currency ?? null, 2) }}
|
||||
{% if n > 1 and unit_build_price is not null %}
|
||||
<span class="ms-1">
|
||||
({% trans %}project.info.per_unit_price{% endtrans %}: {{ unit_build_price | format_money(app.user.currency ?? null, 2) }})
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="get" action="{{ path('project_info', {'id': project.id}) }}" class="mt-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">{% trans %}project.builds.number_of_builds{% endtrans %}</span>
|
||||
<input type="number" min="1" class="form-control" name="n" required value="{{ n }}">
|
||||
<button class="btn btn-outline-secondary" type="submit">{% trans %}project.build.btn_build{% endtrans %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if project.children is not empty %}
|
||||
<div class="mt-1">
|
||||
<h6>
|
||||
|
|
@ -69,9 +95,9 @@
|
|||
</div>
|
||||
|
||||
{% if project.comment is not empty %}
|
||||
<p>
|
||||
<h5>{% trans %}comment.label{% endtrans %}:</h5>
|
||||
{{ project.comment|format_markdown }}
|
||||
</p>
|
||||
<div class="col-12 mt-2">
|
||||
<h5>{% trans %}comment.label{% endtrans %}:</h5>
|
||||
{{ project.comment|format_markdown }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7212,6 +7212,18 @@ Element 1 -> Element 1.2</target>
|
|||
<target>Subprojects</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="prjTtlBP" name="project.info.total_build_price">
|
||||
<segment state="translated">
|
||||
<source>project.info.total_build_price</source>
|
||||
<target>Total build price</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="prjUntBP" name="project.info.per_unit_price">
|
||||
<segment state="translated">
|
||||
<source>project.info.per_unit_price</source>
|
||||
<target>per unit</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="7nV.Cmd" name="project.info.bom_add_parts">
|
||||
<segment state="translated">
|
||||
<source>project.info.bom_add_parts</source>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue