diff --git a/assets/controllers/elements/toggle_visibility_controller.js b/assets/controllers/elements/toggle_visibility_controller.js
index 4600dfb2..51c9cb33 100644
--- a/assets/controllers/elements/toggle_visibility_controller.js
+++ b/assets/controllers/elements/toggle_visibility_controller.js
@@ -7,30 +7,41 @@ export default class extends Controller {
};
connect() {
- this.readableCheckbox = this.element.querySelector("#readable");
+ this.displayCheckbox = this.element.querySelector("#display");
+ this.displaySelect = this.element.querySelector("select#display");
- if (!this.readableCheckbox) {
- return;
+ if (this.displayCheckbox) {
+ this.toggleContainers(this.displayCheckbox.checked);
+
+ this.displayCheckbox.addEventListener("change", (event) => {
+ this.toggleContainers(event.target.checked);
+ });
}
- // Apply the initial visibility state based on the checkbox being checked or not
- this.toggleContainers(this.readableCheckbox.checked);
+ if (this.displaySelect) {
+ this.toggleContainers(this.hasDisplaySelectValue());
+
+ this.displaySelect.addEventListener("change", () => {
+ this.toggleContainers(this.hasDisplaySelectValue());
+ });
+ }
- // Add a change event listener to the 'readable' checkbox
- this.readableCheckbox.addEventListener("change", (event) => {
- // Toggle container visibility when the checkbox value changes
- this.toggleContainers(event.target.checked);
- });
}
/**
- * Toggles the visibility of containers based on the checkbox state.
- * Hides specified containers if the checkbox is checked and shows them otherwise.
- *
- * @param {boolean} isChecked - The current state of the checkbox:
- * true if checked (hide elements), false if unchecked (show them).
+ * Check whether a value was selected in the selectbox
+ * @returns {boolean} True when a value has not been selected that is not empty
*/
- toggleContainers(isChecked) {
+ hasDisplaySelectValue() {
+ return this.displaySelect && this.displaySelect.value !== "";
+ }
+
+ /**
+ * Hides specified containers if the state is active (checkbox checked or select with value).
+ *
+ * @param {boolean} isActive - True when the checkbox is activated or the selectbox has a value.
+ */
+ toggleContainers(isActive) {
if (!Array.isArray(this.classesValue) || this.classesValue.length === 0) {
return;
}
@@ -42,11 +53,10 @@ export default class extends Controller {
return;
}
- // Update the visibility for each selected element
elements.forEach((element) => {
- // If the checkbox is checked, hide the container; otherwise, show it
- element.style.display = isChecked ? "none" : "";
+ element.style.display = isActive ? "none" : "";
});
});
}
-}
\ No newline at end of file
+
+}
diff --git a/src/Helpers/Assemblies/AssemblyPartAggregator.php b/src/Helpers/Assemblies/AssemblyPartAggregator.php
index 94c10257..2346075a 100644
--- a/src/Helpers/Assemblies/AssemblyPartAggregator.php
+++ b/src/Helpers/Assemblies/AssemblyPartAggregator.php
@@ -24,9 +24,16 @@ namespace App\Helpers\Assemblies;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Parts\Part;
+use Dompdf\Dompdf;
+use Dompdf\Options;
+use Twig\Environment;
class AssemblyPartAggregator
{
+ public function __construct(private readonly Environment $twig)
+ {
+ }
+
/**
* Aggregate the required parts and their total quantities for an assembly.
*
@@ -80,4 +87,181 @@ class AssemblyPartAggregator
}
}
}
+
+ /**
+ * Exports a hierarchical Bill of Materials (BOM) for assemblies and parts in a readable format,
+ * including the multiplier for each part and assembly.
+ *
+ * @param Assembly $assembly The root assembly to export.
+ * @param string $indentationSymbol The symbol used for indentation (e.g., ' ').
+ * @param int $initialDepth The starting depth for formatting (default: 0).
+ * @return string Human-readable hierarchical BOM list.
+ */
+ public function exportReadableHierarchy(Assembly $assembly, string $indentationSymbol = ' ', int $initialDepth = 0): string
+ {
+ // Start building the hierarchy
+ $output = '';
+ $this->processAssemblyHierarchy($assembly, $initialDepth, 1, $indentationSymbol, $output);
+
+ return $output;
+ }
+
+ public function exportReadableHierarchyForPdf(array $assemblyHierarchies): string
+ {
+ $html = $this->twig->render('assemblies/export_bom_pdf.html.twig', [
+ 'assemblies' => $assemblyHierarchies,
+ ]);
+
+ $options = new Options();
+ $options->set('isHtml5ParserEnabled', true);
+ $options->set('isPhpEnabled', true);
+
+ $dompdf = new Dompdf($options);
+ $dompdf->loadHtml($html);
+ $dompdf->setPaper('A4');
+ $dompdf->render();
+
+ $canvas = $dompdf->getCanvas();
+ $font = $dompdf->getFontMetrics()->getFont('Arial', 'normal');
+
+ return $dompdf->output();
+ }
+
+ /**
+ * Recursive method to process assemblies and their parts.
+ *
+ * @param Assembly $assembly The current assembly to process.
+ * @param int $depth The current depth in the hierarchy.
+ * @param float $parentMultiplier The multiplier inherited from the parent (default is 1 for root).
+ * @param string $indentationSymbol The symbol used for indentation.
+ * @param string &$output The cumulative output string.
+ */
+ private function processAssemblyHierarchy(Assembly $assembly, int $depth, float $parentMultiplier, string $indentationSymbol, string &$output): void
+ {
+ // Add the current assembly to the output
+ if ($depth === 0) {
+ $output .= sprintf(
+ "%sAssembly: %s [IPN: %s]\n\n",
+ str_repeat($indentationSymbol, $depth),
+ $assembly->getName(),
+ $assembly->getIpn(),
+ );
+ } else {
+ $output .= sprintf(
+ "%sAssembly: %s [IPN: %s, Multiplier: %.2f]\n\n",
+ str_repeat($indentationSymbol, $depth),
+ $assembly->getName(),
+ $assembly->getIpn(),
+ $parentMultiplier
+ );
+ }
+
+ // Gruppiere BOM-Einträge in Kategorien
+ $parts = [];
+ $referencedAssemblies = [];
+ $others = [];
+
+ foreach ($assembly->getBomEntries() as $bomEntry) {
+ if ($bomEntry->getPart() instanceof Part) {
+ $parts[] = $bomEntry;
+ } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) {
+ $referencedAssemblies[] = $bomEntry;
+ } else {
+ $others[] = $bomEntry;
+ }
+ }
+
+ if (!empty($parts)) {
+ // Process each BOM entry for the current assembly
+ foreach ($parts as $bomEntry) {
+ $effectiveQuantity = $bomEntry->getQuantity() * $parentMultiplier;
+
+ $output .= sprintf(
+ "%sPart: %s [IPN: %s, MPNR: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n",
+ str_repeat($indentationSymbol, $depth + 1),
+ $bomEntry->getPart()?->getName(),
+ $bomEntry->getPart()?->getIpn() ?? '-',
+ $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-',
+ $bomEntry->getQuantity(),
+ $parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '',
+ $effectiveQuantity,
+ );
+ }
+
+ $output .= "\n";
+ }
+
+ foreach ($referencedAssemblies as $bomEntry) {
+ // Add referenced assembly details
+ $referencedQuantity = $bomEntry->getQuantity() * $parentMultiplier;
+
+ $output .= sprintf(
+ "%sReferenced Assembly: %s [IPN: %s, Quantity: %.2f%s, EffectiveQuantity: %.2f]\n",
+ str_repeat($indentationSymbol, $depth + 1),
+ $bomEntry->getReferencedAssembly()->getName(),
+ $bomEntry->getReferencedAssembly()->getIpn() ?? '-',
+ $bomEntry->getQuantity(),
+ $parentMultiplier > 1 ? sprintf(", Multiplier: %.2f", $parentMultiplier) : '',
+ $referencedQuantity,
+ );
+
+ // Recurse into the referenced assembly
+ $this->processAssemblyHierarchy(
+ $bomEntry->getReferencedAssembly(),
+ $depth + 2, // Increase depth for nested assemblies
+ $referencedQuantity, // Pass the calculated multiplier
+ $indentationSymbol,
+ $output
+ );
+ }
+
+ foreach ($others as $bomEntry) {
+ $output .= sprintf(
+ "%sOther: %s [Quantity: %.2f, Multiplier: %.2f]\n",
+ str_repeat($indentationSymbol, $depth + 1),
+ $bomEntry->getName(),
+ $bomEntry->getQuantity(),
+ $parentMultiplier,
+ );
+ }
+ }
+
+ public function processAssemblyHierarchyForPdf(Assembly $assembly, int $depth, float $quantity, float $parentMultiplier): array
+ {
+ $result = [
+ 'name' => $assembly->getName(),
+ 'ipn' => $assembly->getIpn(),
+ 'quantity' => $quantity,
+ 'multiplier' => $depth === 0 ? null : $parentMultiplier,
+ 'parts' => [],
+ 'referencedAssemblies' => [],
+ 'others' => [],
+ ];
+
+ foreach ($assembly->getBomEntries() as $bomEntry) {
+ if ($bomEntry->getPart() instanceof Part) {
+ $result['parts'][] = [
+ 'name' => $bomEntry->getPart()->getName(),
+ 'ipn' => $bomEntry->getPart()->getIpn(),
+ 'quantity' => $bomEntry->getQuantity(),
+ 'effectiveQuantity' => $bomEntry->getQuantity() * $parentMultiplier,
+ ];
+ } elseif ($bomEntry->getReferencedAssembly() instanceof Assembly) {
+ $result['referencedAssemblies'][] = $this->processAssemblyHierarchyForPdf(
+ $bomEntry->getReferencedAssembly(),
+ $depth + 1,
+ $bomEntry->getQuantity(),
+ $parentMultiplier * $bomEntry->getQuantity()
+ );
+ } else {
+ $result['others'][] = [
+ 'name' => $bomEntry->getName(),
+ 'quantity' => $bomEntry->getQuantity(),
+ 'multiplier' => $parentMultiplier,
+ ];
+ }
+ }
+
+ return $result;
+ }
}
diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php
index 4b5658f1..d1fb6cda 100644
--- a/src/Services/ImportExportSystem/EntityExporter.php
+++ b/src/Services/ImportExportSystem/EntityExporter.php
@@ -81,8 +81,9 @@ class EntityExporter
$resolver->setDefault('include_children', false);
$resolver->setAllowedTypes('include_children', 'bool');
- $resolver->setDefault('readable', false);
- $resolver->setAllowedTypes('readable', 'bool');
+ $resolver->setDefault('readableSelect', null);
+ $resolver->setAllowedValues('readableSelect', [null, 'readable', 'readable_bom']);
+
}
/**
@@ -240,7 +241,7 @@ class EntityExporter
$entities = [$entities];
}
- if ($request->get('readable', false)) {
+ if ($request->get('readableSelect', false) === 'readable') {
// Map entity classes to export functions
$entityExportMap = [
AttachmentType::class => fn($entities) => $this->exportReadable($entities, AttachmentType::class),
@@ -273,6 +274,23 @@ class EntityExporter
$options['format'] = 'csv';
$options['level'] = 'readable';
+ } if ($request->get('readableSelect', false) === 'readable_bom') {
+ $hierarchies = [];
+
+ foreach ($entities as $entity) {
+ if (!$entity instanceof Assembly) {
+ throw new InvalidArgumentException('Only assemblies can be exported in readable BOM format');
+ }
+
+ $hierarchies[] = $this->assemblyPartAggregator->processAssemblyHierarchyForPdf($entity, 0, 1, 1);
+ }
+
+ $pdfContent = $this->assemblyPartAggregator->exportReadableHierarchyForPdf($hierarchies);
+
+ $response = new Response($pdfContent);
+
+ $options['format'] = 'pdf';
+ $options['level'] = 'readable_bom';
} else {
//Do the serialization with the given options
$serialized_data = $this->exportEntities($entities, $options);
@@ -294,6 +312,7 @@ class EntityExporter
'json' => 'application/json',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xls' => 'application/vnd.ms-excel',
+ 'pdf' => 'application/pdf',
default => 'text/plain',
};
$response->headers->set('Content-Type', $content_type);
diff --git a/templates/admin/_export_form.html.twig b/templates/admin/_export_form.html.twig
index 52751864..4810f67f 100644
--- a/templates/admin/_export_form.html.twig
+++ b/templates/admin/_export_form.html.twig
@@ -35,13 +35,13 @@
-
-
-
-
- {% trans %}export.readable{% endtrans %}
-
-
+
{% trans %}export.readable.label{% endtrans %}
+
+
+
+ {% trans %}export.readable{% endtrans %}
+ {% trans %}export.readable_bom{% endtrans %}
+
@@ -50,4 +50,4 @@
{% trans %}export.btn{% endtrans %}
-
\ No newline at end of file
+
diff --git a/templates/assemblies/export_bom_pdf.html.twig b/templates/assemblies/export_bom_pdf.html.twig
new file mode 100644
index 00000000..15bf5d88
--- /dev/null
+++ b/templates/assemblies/export_bom_pdf.html.twig
@@ -0,0 +1,103 @@
+
+
+
+ Assembly Hierarchy
+
+
+
+
+
+Table of Contents
+
+
+
+ #
+ Assembly Name
+ IPN
+ Section
+
+
+
+ {% for assembly in assemblies %}
+
+ {{ loop.index }}
+ Assembly: {{ assembly.name }}
+ {% if assembly.ipn != '' %}{{ assembly.ipn }}{% else %}-{% endif %}
+ {{ loop.index + 1 }}
+
+ {% endfor %}
+
+
+
+
+
+{% for assembly in assemblies %}
+
+
+
+
+ Name
+ IPN
+ Quantity
+ Multiplier
+ Effective Quantity
+
+
+
+ {% for part in assembly.parts %}
+
+ {{ part.name }}
+ {{ part.ipn }}
+ {{ part.quantity }}
+ {% if assembly.multiplier %}{{ assembly.multiplier }}{% else %}-{% endif %}
+ {{ part.effectiveQuantity }}
+
+ {% endfor %}
+ {% for other in assembly.others %}
+
+ {{ other.name }}
+ {{ other.ipn }}
+ {{ other.quantity }}
+ {{ other.multiplier }}
+ {{ other.effectiveQuantity }}
+
+ {% endfor %}
+ {% for referencedAssembly in assembly.referencedAssemblies %}
+
+ {{ referencedAssembly.name }}
+ {{ referencedAssembly.ipn }}
+ {{ referencedAssembly.quantity }}
+
+ {{ referencedAssembly.quantity }}
+
+ {% endfor %}
+
+
+
+ {% for refAssembly in assembly.referencedAssemblies %}
+ {% include 'assemblies/export_bom_referenced_assembly_pdf.html.twig' with {'assembly': refAssembly} only %}
+ {% endfor %}
+
+ {% if not loop.last %}
+
+ {% endif %}
+
+
+{% endfor %}
+
+
diff --git a/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig b/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig
new file mode 100644
index 00000000..b5a1324d
--- /dev/null
+++ b/templates/assemblies/export_bom_referenced_assembly_pdf.html.twig
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+ Type
+ Name
+ IPN
+ Quantity
+ Multiplier
+ Effective Quantity
+
+
+
+ {% for part in assembly.parts %}
+
+ Part
+ {{ part.name }}
+ {{ part.ipn }}
+ {{ part.quantity }}
+ {% if assembly.multiplier %}{{ assembly.multiplier }}{% else %}-{% endif %}
+ {{ part.effectiveQuantity }}
+
+ {% endfor %}
+
+ {% for other in assembly.others %}
+
+ Other
+ {{ other.name }}
+ -
+ {{ other.quantity }}
+ {{ other.multiplier }}
+ -
+
+ {% endfor %}
+
+ {% for referencedAssembly in assembly.referencedAssemblies %}
+
+ Referenced assembly
+ {{ referencedAssembly.name }}
+ -
+ {{ referencedAssembly.quantity }}
+
+ {{ referencedAssembly.multiplier }}
+
+ {% endfor %}
+
+
+
+
+ {% for refAssembly in assembly.referencedAssemblies %}
+ {% include 'assemblies/export_bom_referenced_assembly_pdf.html.twig' with {'assembly': refAssembly} only %}
+ {% endfor %}
+
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf
index c5f9000a..ad2d4bb7 100644
--- a/translations/messages.cs.xlf
+++ b/translations/messages.cs.xlf
@@ -351,10 +351,22 @@
Exportovat všechny prvky
+
+
+ export.readable.label
+ Čitelný export
+
+
export.readable
- Čitelné CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf
index 4518c801..c07d229d 100644
--- a/translations/messages.da.xlf
+++ b/translations/messages.da.xlf
@@ -351,10 +351,22 @@
Eksportér alle elementer
+
+
+ export.readable.label
+ Læsbar eksport
+
+
export.readable
- Læsbar CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf
index 31b5f5e9..7f4f5d33 100644
--- a/translations/messages.de.xlf
+++ b/translations/messages.de.xlf
@@ -1004,10 +1004,22 @@ Subelemente werden beim Löschen nach oben verschoben.
Unterelemente auch exportieren
+
+
+ export.readable.label
+ Lesbarer Export
+
+
export.readable
- Lesbares CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf
index fecbac21..b9be2574 100644
--- a/translations/messages.el.xlf
+++ b/translations/messages.el.xlf
@@ -228,10 +228,22 @@
Εξαγωγή όλων των στοιχείων
+
+
+ export.readable.label
+ Αναγνώσιμη εξαγωγή
+
+
export.readable
- Αναγνώσιμο CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 1295c253..7a78c01a 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -351,10 +351,22 @@
Export all elements
+
+
+ export.readable.label
+ Readable Export
+
+
export.readable
- Readable CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf
index 0268e83c..8539d631 100644
--- a/translations/messages.es.xlf
+++ b/translations/messages.es.xlf
@@ -351,10 +351,22 @@
Exportar todos los elementos
+
+
+ export.readable.label
+ Exportación legible
+
+
export.readable
- CSV legible
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf
index 44af739d..ae9e9aff 100644
--- a/translations/messages.fr.xlf
+++ b/translations/messages.fr.xlf
@@ -320,10 +320,22 @@
Exporter tous les éléments
+
+
+ export.readable.label
+ Export lisible
+
+
export.readable
- CSV lisible
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf
index 70ae96f2..e7b012ed 100644
--- a/translations/messages.it.xlf
+++ b/translations/messages.it.xlf
@@ -351,10 +351,22 @@
Esportare tutti gli elementi
+
+
+ export.readable.label
+ Esporta leggibile
+
+
export.readable
- CSV leggibile
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf
index 13ced250..4d92f055 100644
--- a/translations/messages.ja.xlf
+++ b/translations/messages.ja.xlf
@@ -320,10 +320,22 @@
すべてエクスポートする
+
+
+ export.readable.label
+ 読みやすいエクスポート
+
+
export.readable
- 読みやすいCSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf
index 5d473dc2..63a42c6c 100644
--- a/translations/messages.nl.xlf
+++ b/translations/messages.nl.xlf
@@ -351,10 +351,22 @@
Exporteer alle elementen
+
+
+ export.readable.label
+ Leesbare export
+
+
export.readable
- Leesbare CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf
index 88ba9a0a..2c77cfea 100644
--- a/translations/messages.pl.xlf
+++ b/translations/messages.pl.xlf
@@ -351,10 +351,22 @@
Eksportuj wszystkie elementy
+
+
+ export.readable.label
+ Eksport czytelny
+
+
export.readable
- Czytelny CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf
index 4d0b102b..4345c6c9 100644
--- a/translations/messages.ru.xlf
+++ b/translations/messages.ru.xlf
@@ -351,10 +351,22 @@
Экспортировать всё
+
+
+ export.readable.label
+ Читаемый экспорт
+
+
export.readable
- Читаемый CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF
diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf
index 6fc40930..34da9e78 100644
--- a/translations/messages.zh.xlf
+++ b/translations/messages.zh.xlf
@@ -351,10 +351,22 @@
导出所有元素
+
+
+ export.readable.label
+ 可读导出
+
+
export.readable
- 可读的 CSV
+ CSV
+
+
+
+
+ export.readable_bom
+ PDF