Skip to content

Commit 7befa53

Browse files
author
Marcel Diegelmann
committed
Erweitere Exportfunktion um lesbare BOM-Option (PDF-Ausgabe).
Neue Auswahloption "Lesbarer Export" hinzugefügt, die den Export hierarchischer Baugruppen als PDF ermöglicht.
1 parent 6bb20fb commit 7befa53

19 files changed

+572
-43
lines changed

assets/controllers/elements/toggle_visibility_controller.js

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,41 @@ export default class extends Controller {
77
};
88

99
connect() {
10-
this.readableCheckbox = this.element.querySelector("#readable");
10+
this.displayCheckbox = this.element.querySelector("#display");
11+
this.displaySelect = this.element.querySelector("select#display");
1112

12-
if (!this.readableCheckbox) {
13-
return;
13+
if (this.displayCheckbox) {
14+
this.toggleContainers(this.displayCheckbox.checked);
15+
16+
this.displayCheckbox.addEventListener("change", (event) => {
17+
this.toggleContainers(event.target.checked);
18+
});
1419
}
1520

16-
// Apply the initial visibility state based on the checkbox being checked or not
17-
this.toggleContainers(this.readableCheckbox.checked);
21+
if (this.displaySelect) {
22+
this.toggleContainers(this.hasDisplaySelectValue());
1823

19-
// Add a change event listener to the 'readable' checkbox
20-
this.readableCheckbox.addEventListener("change", (event) => {
21-
// Toggle container visibility when the checkbox value changes
22-
this.toggleContainers(event.target.checked);
23-
});
24+
this.displaySelect.addEventListener("change", () => {
25+
this.toggleContainers(this.hasDisplaySelectValue());
26+
});
27+
}
28+
29+
}
30+
31+
/**
32+
* Check whether a value was selected in the selectbox
33+
* @returns {boolean} True when a value has not been selected that is not empty
34+
*/
35+
hasDisplaySelectValue() {
36+
return this.displaySelect && this.displaySelect.value !== "";
2437
}
2538

2639
/**
27-
* Toggles the visibility of containers based on the checkbox state.
28-
* Hides specified containers if the checkbox is checked and shows them otherwise.
40+
* Hides specified containers if the state is active (checkbox checked or select with value).
2941
*
30-
* @param {boolean} isChecked - The current state of the checkbox:
31-
* true if checked (hide elements), false if unchecked (show them).
42+
* @param {boolean} isActive - True when the checkbox is activated or the selectbox has a value.
3243
*/
33-
toggleContainers(isChecked) {
44+
toggleContainers(isActive) {
3445
if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) {
3546
return;
3647
}
@@ -42,11 +53,10 @@ export default class extends Controller {
4253
return;
4354
}
4455

45-
// Update the visibility for each selected element
4656
elements.forEach((element) => {
47-
// If the checkbox is checked, hide the container; otherwise, show it
48-
element.style.display = isChecked ? "none" : "";
57+
element.style.display = isActive ? "none" : "";
4958
});
5059
});
5160
}
52-
}
61+
62+
}

src/Helpers/Assemblies/AssemblyPartAggregator.php

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@
2424

2525
use App\Entity\AssemblySystem\Assembly;
2626
use App\Entity\Parts\Part;
27+
use Dompdf\Dompdf;
28+
use Dompdf\Options;
29+
use Twig\Environment;
2730

2831
class AssemblyPartAggregator
2932
{
33+
public function __construct(private readonly Environment $twig)
34+
{
35+
}
36+
3037
/**
3138
* Aggregate the required parts and their total quantities for an assembly.
3239
*
@@ -80,4 +87,181 @@ private function processAssembly(Assembly $assembly, float $multiplier, array &$
8087
}
8188
}
8289
}
90+
91+
/**
92+
* Exports a hierarchical Bill of Materials (BOM) for assemblies and parts in a readable format,
93+
* including the multiplier for each part and assembly.
94+
*
95+
* @param Assembly $assembly The root assembly to export.
96+
* @param string $indentationSymbol The symbol used for indentation (e.g., ' ').
97+
* @param int $initialDepth The starting depth for formatting (default: 0).
98+
* @return string Human-readable hierarchical BOM list.
99+
*/
100+
public function exportReadableHierarchy(Assembly $assembly, string $indentationSymbol = ' ', int $initialDepth = 0): string
101+
{
102+
// Start building the hierarchy
103+
$output = '';
104+
$this->processAssemblyHierarchy($assembly, $initialDepth, 1, $indentationSymbol, $output);
105+
106+
return $output;
107+
}
108+
109+
public function exportReadableHierarchyForPdf(array $assemblyHierarchies): string
110+
{
111+
$html = $this->twig->render('assemblies/export_bom_pdf.html.twig', [
112+
'assemblies' => $assemblyHierarchies,
113+
]);
114+
115+
$options = new Options();
116+
$options->set('isHtml5ParserEnabled', true);
117+
$options->set('isPhpEnabled', true);
118+
119+
$dompdf = new Dompdf($options);
120+
$dompdf->loadHtml($html);
121+
$dompdf->setPaper('A4');
122+
$dompdf->render();
123+
124+
$canvas = $dompdf->getCanvas();
125+
$font = $dompdf->getFontMetrics()->getFont('Arial', 'normal');
126+
127+
return $dompdf->output();
128+
}
129+
130+
/**
131+
* Recursive method to process assemblies and their parts.
132+
*
133+
* @param Assembly $assembly The current assembly to process.
134+
* @param int $depth The current depth in the hierarchy.
135+
* @param float $parentMultiplier The multiplier inherited from the parent (default is 1 for root).
136+
* @param string $indentationSymbol The symbol used for indentation.
137+
* @param string &$output The cumulative output string.
138+
*/
139+
private function processAssemblyHierarchy(Assembly $assembly, int $depth, float $parentMultiplier, string $indentationSymbol, string &$output): void
140+
{
141+
// Add the current assembly to the output
142+
if ($depth === 0) {
143+
$output .= sprintf(
144+
"%sAssembly: %s [IPN: %s]\n\n",
145+
str_repeat($indentationSymbol, $depth),
146+
$assembly->getName(),
147+
$assembly->getIpn(),
148+
);
149+
} else {
150+
$output .= sprintf(
151+
"%sAssembly: %s [IPN: %s, Multiplier: %.2f]\n\n",
152+
str_repeat($indentationSymbol, $depth),
153+
$assembly->getName(),
154+
$assembly->getIpn(),
155+
$parentMultiplier
156+
);
157+
}
158+
159+
// Gruppiere BOM-Einträge in Kategorien
160+
$parts = [];
161+
$referencedAssemblies = [];
162+
$others = [];
163+
164+
foreach ($assembly->getBomEntries() as $bomEntry) {
165+
if ($bomEntry->getPart() instanceof Part) {
166+
$parts[] = $bomEntry;
167+
} elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) {
168+
$referencedAssemblies[] = $bomEntry;
169+
} else {
170+
$others[] = $bomEntry;
171+
}
172+
}
173+
174+
if (!empty($parts)) {
175+
// Process each BOM entry for the current assembly
176+
foreach ($parts as $bomEntry) {
177+
$effectiveQuantity = $bomEntry->getQuantity() * $parentMultiplier;
178+
179+
$output .= sprintf(
180+
"%sPart: %s [IPN: %s, MPNR: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n",
181+
str_repeat($indentationSymbol, $depth + 1),
182+
$bomEntry->getPart()?->getName(),
183+
$bomEntry->getPart()?->getIpn() ?? '-',
184+
$bomEntry->getPart()?->getManufacturerProductNumber() ?? '-',
185+
$bomEntry->getQuantity(),
186+
$parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '',
187+
$effectiveQuantity,
188+
);
189+
}
190+
191+
$output .= "\n";
192+
}
193+
194+
foreach ($referencedAssemblies as $bomEntry) {
195+
// Add referenced assembly details
196+
$referencedQuantity = $bomEntry->getQuantity() * $parentMultiplier;
197+
198+
$output .= sprintf(
199+
"%sReferenced Assembly: %s [IPN: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n",
200+
str_repeat($indentationSymbol, $depth + 1),
201+
$bomEntry->getReferencedAssembly()->getName(),
202+
$bomEntry->getReferencedAssembly()->getIpn() ?? '-',
203+
$bomEntry->getQuantity(),
204+
$parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '',
205+
$referencedQuantity,
206+
);
207+
208+
// Recurse into the referenced assembly
209+
$this->processAssemblyHierarchy(
210+
$bomEntry->getReferencedAssembly(),
211+
$depth + 2, // Increase depth for nested assemblies
212+
$referencedQuantity, // Pass the calculated multiplier
213+
$indentationSymbol,
214+
$output
215+
);
216+
}
217+
218+
foreach ($others as $bomEntry) {
219+
$output .= sprintf(
220+
"%sOther: %s [Quantity: %.2f, Multiplier: %.2f]\n",
221+
str_repeat($indentationSymbol, $depth + 1),
222+
$bomEntry->getName(),
223+
$bomEntry->getQuantity(),
224+
$parentMultiplier,
225+
);
226+
}
227+
}
228+
229+
public function processAssemblyHierarchyForPdf(Assembly $assembly, int $depth, float $quantity, float $parentMultiplier): array
230+
{
231+
$result = [
232+
'name' => $assembly->getName(),
233+
'ipn' => $assembly->getIpn(),
234+
'quantity' => $quantity,
235+
'multiplier' => $depth === 0 ? null : $parentMultiplier,
236+
'parts' => [],
237+
'referencedAssemblies' => [],
238+
'others' => [],
239+
];
240+
241+
foreach ($assembly->getBomEntries() as $bomEntry) {
242+
if ($bomEntry->getPart() instanceof Part) {
243+
$result['parts'][] = [
244+
'name' => $bomEntry->getPart()->getName(),
245+
'ipn' => $bomEntry->getPart()->getIpn(),
246+
'quantity' => $bomEntry->getQuantity(),
247+
'effectiveQuantity' => $bomEntry->getQuantity() * $parentMultiplier,
248+
];
249+
} elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) {
250+
$result['referencedAssemblies'][] = $this->processAssemblyHierarchyForPdf(
251+
$bomEntry->getReferencedAssembly(),
252+
$depth + 1,
253+
$bomEntry->getQuantity(),
254+
$parentMultiplier * $bomEntry->getQuantity()
255+
);
256+
} else {
257+
$result['others'][] = [
258+
'name' => $bomEntry->getName(),
259+
'quantity' => $bomEntry->getQuantity(),
260+
'multiplier' => $parentMultiplier,
261+
];
262+
}
263+
}
264+
265+
return $result;
266+
}
83267
}

src/Services/ImportExportSystem/EntityExporter.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ protected function configureOptions(OptionsResolver $resolver): void
7878
$resolver->setDefault('include_children', false);
7979
$resolver->setAllowedTypes('include_children', 'bool');
8080

81-
$resolver->setDefault('readable', false);
82-
$resolver->setAllowedTypes('readable', 'bool');
81+
$resolver->setDefault('readableSelect', null);
82+
$resolver->setAllowedValues('readableSelect', [null, 'readable', 'readable_bom']);
83+
8384
}
8485

8586
/**
@@ -162,7 +163,7 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities,
162163
$entities = [$entities];
163164
}
164165

165-
if ($request->get('readable', false)) {
166+
if ($request->get('readableSelect', false) === 'readable') {
166167
// Map entity classes to export functions
167168
$entityExportMap = [
168169
AttachmentType::class => fn($entities) => $this->exportReadable($entities, AttachmentType::class),
@@ -195,6 +196,23 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities,
195196

196197
$options['format'] = 'csv';
197198
$options['level'] = 'readable';
199+
} if ($request->get('readableSelect', false) === 'readable_bom') {
200+
$hierarchies = [];
201+
202+
foreach ($entities as $entity) {
203+
if (!$entity instanceof Assembly) {
204+
throw new InvalidArgumentException('Only assemblies can be exported in readable BOM format');
205+
}
206+
207+
$hierarchies[] = $this->assemblyPartAggregator->processAssemblyHierarchyForPdf($entity, 0, 1, 1);
208+
}
209+
210+
$pdfContent = $this->assemblyPartAggregator->exportReadableHierarchyForPdf($hierarchies);
211+
212+
$response = new Response($pdfContent);
213+
214+
$options['format'] = 'pdf';
215+
$options['level'] = 'readable_bom';
198216
} else {
199217
//Do the serialization with the given options
200218
$serialized_data = $this->exportEntities($entities, $options);
@@ -221,6 +239,9 @@ public function exportEntityFromRequest(AbstractNamedDBElement|array $entities,
221239
case 'json':
222240
$content_type = 'application/json';
223241
break;
242+
case 'pdf':
243+
$content_type = 'application/pdf';
244+
break;
224245
}
225246
$response->headers->set('Content-Type', $content_type);
226247

templates/admin/_export_form.html.twig

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@
3535
</div>
3636

3737
<div class="row mt-2">
38-
<div class="offset-md-3 col-sm">
39-
<div class="form-check">
40-
<input class="form-check-input form-check-input" name="readable" id="readable" type="checkbox" data-action="change->toggle-visibility#toggle">
41-
<label class="form-check-label form-check-label" for="readable">
42-
{% trans %}export.readable{% endtrans %}
43-
</label>
44-
</div>
38+
<label class="col-form-label col-md-3" for="readableSelect" >{% trans %}export.readable.label{% endtrans %}</label>
39+
<div class="col-md-9">
40+
<select id="display" name="readableSelect" class="form-select" data-action="change->action-handler#handleAction">
41+
<option value="" selected></option>
42+
<option value="readable">{% trans %}export.readable{% endtrans %}</option>
43+
<option value="readable_bom">{% trans %}export.readable_bom{% endtrans %}</option>
44+
</select>
4545
</div>
4646
</div>
4747

@@ -50,4 +50,4 @@
5050
<button type="submit" class="btn btn-primary">{% trans %}export.btn{% endtrans %}</button>
5151
</div>
5252
</div>
53-
</form>
53+
</form>

0 commit comments

Comments
 (0)