Skip to content

Commit 83ad992

Browse files
committed
Added optional "stocked amount" and storage locations columns for the BOM list
This fixes issue #429
1 parent 958d59a commit 83ad992

File tree

4 files changed

+103
-59
lines changed

4 files changed

+103
-59
lines changed

src/DataTables/Helpers/PartDataTableHelper.php

+73-5
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,31 @@
2020
* You should have received a copy of the GNU Affero General Public License
2121
* along with this program. If not, see <https://www.gnu.org/licenses/>.
2222
*/
23+
2324
namespace App\DataTables\Helpers;
2425

26+
use App\Entity\Parts\StorageLocation;
2527
use App\Entity\ProjectSystem\Project;
2628
use App\Entity\Attachments\Attachment;
2729
use App\Entity\Parts\Part;
2830
use App\Services\Attachments\AttachmentURLGenerator;
2931
use App\Services\Attachments\PartPreviewGenerator;
3032
use App\Services\EntityURLGenerator;
33+
use App\Services\Formatters\AmountFormatter;
3134
use Symfony\Contracts\Translation\TranslatorInterface;
3235

3336
/**
3437
* A helper service which contains common code to render columns for part related tables
3538
*/
3639
class PartDataTableHelper
3740
{
38-
public function __construct(private readonly PartPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly EntityURLGenerator $entityURLGenerator, private readonly TranslatorInterface $translator)
39-
{
41+
public function __construct(
42+
private readonly PartPreviewGenerator $previewGenerator,
43+
private readonly AttachmentURLGenerator $attachmentURLGenerator,
44+
private readonly EntityURLGenerator $entityURLGenerator,
45+
private readonly TranslatorInterface $translator,
46+
private readonly AmountFormatter $amountFormatter,
47+
) {
4048
}
4149

4250
public function renderName(Part $context): string
@@ -45,14 +53,16 @@ public function renderName(Part $context): string
4553

4654
//Depending on the part status we show a different icon (the later conditions have higher priority)
4755
if ($context->isFavorite()) {
48-
$icon = sprintf('<i class="fa-solid fa-star fa-fw me-1" title="%s"></i>', $this->translator->trans('part.favorite.badge'));
56+
$icon = sprintf('<i class="fa-solid fa-star fa-fw me-1" title="%s"></i>',
57+
$this->translator->trans('part.favorite.badge'));
4958
}
5059
if ($context->isNeedsReview()) {
51-
$icon = sprintf('<i class="fa-solid fa-ambulance fa-fw me-1" title="%s"></i>', $this->translator->trans('part.needs_review.badge'));
60+
$icon = sprintf('<i class="fa-solid fa-ambulance fa-fw me-1" title="%s"></i>',
61+
$this->translator->trans('part.needs_review.badge'));
5262
}
5363
if ($context->getBuiltProject() instanceof Project) {
5464
$icon = sprintf('<i class="fa-solid fa-box-archive fa-fw me-1" title="%s"></i>',
55-
$this->translator->trans('part.info.projectBuildPart.hint') . ': ' . $context->getBuiltProject()->getName());
65+
$this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName());
5666
}
5767

5868

@@ -85,4 +95,62 @@ public function renderPicture(Part $context): string
8595
$title
8696
);
8797
}
98+
99+
public function renderStorageLocations(Part $context): string
100+
{
101+
$tmp = [];
102+
foreach ($context->getPartLots() as $lot) {
103+
//Ignore lots without storelocation
104+
if (!$lot->getStorageLocation() instanceof StorageLocation) {
105+
continue;
106+
}
107+
$tmp[] = sprintf(
108+
'<a href="%s" title="%s">%s</a>',
109+
$this->entityURLGenerator->listPartsURL($lot->getStorageLocation()),
110+
htmlspecialchars($lot->getStorageLocation()->getFullPath()),
111+
htmlspecialchars($lot->getStorageLocation()->getName())
112+
);
113+
}
114+
115+
return implode('<br>', $tmp);
116+
}
117+
118+
public function renderAmount(Part $context): string
119+
{
120+
$amount = $context->getAmountSum();
121+
$expiredAmount = $context->getExpiredAmountSum();
122+
123+
$ret = '';
124+
125+
if ($context->isAmountUnknown()) {
126+
//When all amounts are unknown, we show a question mark
127+
if ($amount === 0.0) {
128+
$ret .= sprintf('<b class="text-primary" title="%s">?</b>',
129+
$this->translator->trans('part_lots.instock_unknown'));
130+
} else { //Otherwise mark it with greater equal and the (known) amount
131+
$ret .= sprintf('<b class="text-primary" title="%s">≥</b>',
132+
$this->translator->trans('part_lots.instock_unknown')
133+
);
134+
$ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit()));
135+
}
136+
} else {
137+
$ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit()));
138+
}
139+
140+
//If we have expired lots, we show them in parentheses behind
141+
if ($expiredAmount > 0) {
142+
$ret .= sprintf(' <span title="%s" class="text-muted">(+%s)</span>',
143+
$this->translator->trans('part_lots.is_expired'),
144+
htmlspecialchars($this->amountFormatter->format($expiredAmount, $context->getPartUnit())));
145+
}
146+
147+
//When the amount is below the minimum amount, we highlight the number red
148+
if ($context->isNotEnoughInstock()) {
149+
$ret = sprintf('<b class="text-danger" title="%s">%s</b>',
150+
$this->translator->trans('part.info.amount.less_than_desired'),
151+
$ret);
152+
}
153+
154+
return $ret;
155+
}
88156
}

src/DataTables/PartsDataTable.php

+2-54
Original file line numberDiff line numberDiff line change
@@ -139,63 +139,11 @@ public function configure(DataTable $dataTable, array $options): void
139139
->add('storelocation', TextColumn::class, [
140140
'label' => $this->translator->trans('part.table.storeLocations'),
141141
'orderField' => 'storelocations.name',
142-
'render' => function ($value, Part $context): string {
143-
$tmp = [];
144-
foreach ($context->getPartLots() as $lot) {
145-
//Ignore lots without storelocation
146-
if (!$lot->getStorageLocation() instanceof StorageLocation) {
147-
continue;
148-
}
149-
$tmp[] = sprintf(
150-
'<a href="%s" title="%s">%s</a>',
151-
$this->urlGenerator->listPartsURL($lot->getStorageLocation()),
152-
htmlspecialchars($lot->getStorageLocation()->getFullPath()),
153-
htmlspecialchars($lot->getStorageLocation()->getName())
154-
);
155-
}
156-
157-
return implode('<br>', $tmp);
158-
},
142+
'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
159143
], alias: 'storage_location')
160144
->add('amount', TextColumn::class, [
161145
'label' => $this->translator->trans('part.table.amount'),
162-
'render' => function ($value, Part $context) {
163-
$amount = $context->getAmountSum();
164-
$expiredAmount = $context->getExpiredAmountSum();
165-
166-
$ret = '';
167-
168-
if ($context->isAmountUnknown()) {
169-
//When all amounts are unknown, we show a question mark
170-
if ($amount === 0.0) {
171-
$ret .= sprintf('<b class="text-primary" title="%s">?</b>',
172-
$this->translator->trans('part_lots.instock_unknown'));
173-
} else { //Otherwise mark it with greater equal and the (known) amount
174-
$ret .= sprintf('<b class="text-primary" title="%s">≥</b>',
175-
$this->translator->trans('part_lots.instock_unknown')
176-
);
177-
$ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit()));
178-
}
179-
} else {
180-
$ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit()));
181-
}
182-
183-
//If we have expired lots, we show them in parentheses behind
184-
if ($expiredAmount > 0) {
185-
$ret .= sprintf(' <span title="%s" class="text-muted">(+%s)</span>',
186-
$this->translator->trans('part_lots.is_expired'),
187-
htmlspecialchars($this->amountFormatter->format($expiredAmount, $context->getPartUnit())));
188-
}
189-
190-
//When the amount is below the minimum amount, we highlight the number red
191-
if ($context->isNotEnoughInstock()) {
192-
$ret = sprintf('<b class="text-danger" title="%s">%s</b>',
193-
$this->translator->trans('part.info.amount.less_than_desired'),
194-
$ret);
195-
}
196-
197-
return $ret;
198-
},
146+
'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderAmount($context),
199147
'orderField' => 'amountSum'
200148
])
201149
->add('minamount', TextColumn::class, [

src/DataTables/ProjectBomEntriesDataTable.php

+22
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,28 @@ public function configure(DataTable $dataTable, array $options): void
151151
},
152152
])
153153

154+
->add('instockAmount', TextColumn::class, [
155+
'label' => 'project.bom.instockAmount',
156+
'visible' => false,
157+
'render' => function ($value, ProjectBOMEntry $context) {
158+
if ($context->getPart()) {
159+
return $this->partDataTableHelper->renderAmount($context->getPart());
160+
}
161+
162+
return '';
163+
}
164+
])
165+
->add('storageLocations', TextColumn::class, [
166+
'label' => 'part.table.storeLocations',
167+
'visible' => false,
168+
'render' => function ($value, ProjectBOMEntry $context) {
169+
if ($context->getPart()) {
170+
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
171+
}
172+
173+
return '';
174+
}
175+
])
154176

155177
->add('addedDate', LocaleDateTimeColumn::class, [
156178
'label' => $this->translator->trans('part.table.addedDate'),

translations/messages.en.xlf

+6
Original file line numberDiff line numberDiff line change
@@ -11951,5 +11951,11 @@ Please note, that you can not impersonate a disabled user. If you try you will g
1195111951
<target>Vendor barcode (configured in part lot)</target>
1195211952
</segment>
1195311953
</unit>
11954+
<unit id="ilCTQni" name="project.bom.instockAmount">
11955+
<segment>
11956+
<source>project.bom.instockAmount</source>
11957+
<target>Stocked amount</target>
11958+
</segment>
11959+
</unit>
1195411960
</file>
1195511961
</xliff>

0 commit comments

Comments
 (0)