Repository: inventree/InvenTree

Issue:
[FR] :checkered_flag: Export BOM in CSV with alternative Part numbers
### Please verify that this feature request has NOT been suggested before.

- [X] I checked and didn't find similar feature request

### Problem statement

It would be a great feature if when we are exporting BOM in CSV to share with other people full BOM with alternative part numbers listed as well

### Suggested solution

It could be as simple as: "ALT PN 1, ALT DESC 1, ALT PN 2, ALT DESC 2" etc.

### Describe alternatives you've considered

Currently we manually enter alts

### Examples of other systems

_No response_

### Do you want to develop this?

- [ ] I want to develop this.


Discussion:
Sounds reasonable.
The priorities of the core team are not really in this area of the code right now (see https://inventree.org/blog/2023/01/02/upcoming) but maybe someone else comes around and develops it. Should be a good starter project as the export code is rather simple.

Resolving patch:
diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py
--- a/InvenTree/part/bom.py
+++ b/InvenTree/part/bom.py
@@ -12,7 +12,7 @@
                                str2bool)
 
 from .admin import BomItemResource
-from .models import BomItem, Part
+from .models import BomItem, BomItemSubstitute, Part
 
 
 def IsValidBOMFormat(fmt):
@@ -58,6 +58,7 @@ def ExportBom(part: Part, fmt='csv', cascade: bool = False, max_levels: int = No
         supplier_data (bool, optional): Additonal data that should be added. Defaults to False.
         manufacturer_data (bool, optional): Additonal data that should be added. Defaults to False.
         pricing_data (bool, optional): Include pricing data in exported BOM. Defaults to False
+        substitute_part_data (bool, optional): Include substitute part numbers in exported BOM. Defaults to False
 
     Returns:
         StreamingHttpResponse: Response that can be passed to the endpoint
@@ -68,6 +69,7 @@ def ExportBom(part: Part, fmt='csv', cascade: bool = False, max_levels: int = No
     supplier_data = str2bool(kwargs.get('supplier_data', False))
     manufacturer_data = str2bool(kwargs.get('manufacturer_data', False))
     pricing_data = str2bool(kwargs.get('pricing_data', False))
+    substitute_part_data = str2bool(kwargs.get('substitute_part_data', False))
 
     if not IsValidBOMFormat(fmt):
         fmt = 'csv'
@@ -112,6 +114,37 @@ def add_columns_to_dataset(columns, column_size):
         except AttributeError:
             pass
 
+    if substitute_part_data:
+        """If requested, add extra columns for all substitute part numbers associated with each line item."""
+
+        col_index = 0
+        substitute_cols = {}
+
+        for bom_item in bom_items:
+            substitutes = BomItemSubstitute.objects.filter(bom_item=bom_item)
+            for s_idx, substitute in enumerate(substitutes):
+
+                """Create substitute part name column"""
+                name = f'{_("Substitute Part")}{s_idx + 1}'
+                value = substitute.part.name
+                try:
+                    substitute_cols[name].update({col_index: value})
+                except KeyError:
+                    substitute_cols[name] = {col_index: value}
+
+                """Create substitute part description column"""
+                name = f'{_("Substitute Description")}{s_idx + 1}'
+                value = substitute.part.description
+                try:
+                    substitute_cols[name].update({col_index: value})
+                except KeyError:
+                    substitute_cols[name] = {col_index: value}
+
+            col_index = col_index + 1
+
+        # Add substitute columns to dataset
+        add_columns_to_dataset(substitute_cols, len(bom_items))
+
     if parameter_data:
         """If requested, add extra columns for each PartParameter associated with each line item."""
 
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -468,6 +468,8 @@ def get(self, request, *args, **kwargs):
 
         parameter_data = str2bool(request.GET.get('parameter_data', False))
 
+        substitute_part_data = str2bool(request.GET.get('substitute_part_data', False))
+
         stock_data = str2bool(request.GET.get('stock_data', False))
 
         supplier_data = str2bool(request.GET.get('supplier_data', False))
@@ -500,10 +502,11 @@ def get(self, request, *args, **kwargs):
                          supplier_data=supplier_data,
                          manufacturer_data=manufacturer_data,
                          pricing_data=pricing_data,
+                         substitute_part_data=substitute_part_data,
                          )
 
     def get_data(self):
-        """Return a cutsom message"""
+        """Return a custom message"""
         return {
             'info': 'Exported BOM'
         }


Test patch: