Repository: inventree/InvenTree

Issue:
Variant Part Parameters: should they be copied over from Template part?
This is first a question before being an issue. I realized parameters were not copied over from the Template part, shouldn't they be?
My preference would be that they are copied at the creation of the part variant, but their presence and value post-creation are not tracked with reference to the Template part. That way one can setup the Template part with placeholder parameters and they can easily be edited in the variants of that part.

@SchrodingersGat What do you think?
If you are on-board, happy to take a look on the implementation side.

EDIT: reformulation to match InvenTree's language


Discussion:
Umm it seems like Duplicating also leaves Parameters out of the copied part... Is that intentional too?
Now that you mention this, it seems exceedingly obvious as a good feature! It's current lack is not intentional.

> If you are on-board, happy to take a look on the implementation side.

This would be great!
Is the idea with Template parts and Variants to have, e.g., a Template that represents Metal Film resistors and then Variants are the various specific values, etc, each of which has its own multiple potential Supplier Parts?  Or is a Template intended to act as a sort of Meta-Part that has various manufacturers / suppliers, with each Variant representing (e.g.) an On Semi or a Central Semi version of a particular BJT type? 
@rco3 My personal take on this:
- a Template part holds information that can be re-used across "variants" of that part, this is useful in creating a base variant for an assembly that can be derived from, with BoM changes for instance. Variants parts will reference the Template part therefore explicitly creating and showing relationship.
- Template parts shouldn't be used as categories, at least not in InvenTree where categories are separate.
- Component manufacturer alternates should be specified in the Supplier tab, each alternate should have their own entry there. I would create a new Supplier entry **only if** the new manufacturer component's specifications are **exactly** the same as the present ones. If not, I would create a new independent part, not a variant. I could see this leading to a Template part to Variants structure when specifications differ but I personally don't see any use case for this implementation. As long as components are well categorized and have clear search keywords, it does not really matter if a component is a variant of another, specifications would prevail.
@eeintech @rco3 here's how I use variants at my company:

Variants are (for my purposes) "manufacturing variants" where the variant dictates a particular configuration which a customer can order a part. A variant might determine the particular options that come with a part, like harnesses, enclosure, color, specs, etc.

The advantage of using a template / variant system here, rather than just making wholly separate parts in the database, is that these parts are linked in a way that lets you do some special things:

### Serial Numbers

Parts that are linked in a template / variant relationship must have unique serial numbers. e.g. if you have a template part `Widget`, and two variants `Widget-01` and `Widget-02` then any assigned serial numbers must be unique across all these variants.

### Stock Reporting

The "stock" for a template part includes stock for all variants under that part

### Logical Grouping

The template / variant relationship is subtly different to the category / part relationship 
@SchrodingersGat , @eeintech : thank you both very much.  This makes it all very clear how the template/variant logic works, and the expected use case (which I find quite useful).

Resolving patch:
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -129,10 +129,17 @@ class EditPartForm(HelperForm):
         'IPN': 'fa-hashtag',
     }
 
-    deep_copy = forms.BooleanField(required=False,
-                                   initial=True,
-                                   help_text=_("Perform 'deep copy' which will duplicate all BOM data for this part"),
-                                   widget=forms.HiddenInput())
+    bom_copy = forms.BooleanField(required=False,
+                                  initial=True,
+                                  help_text=_("Duplicate all BOM data for this part"),
+                                  label=_('Copy BOM'),
+                                  widget=forms.HiddenInput())
+
+    parameters_copy = forms.BooleanField(required=False,
+                                         initial=True,
+                                         help_text=_("Duplicate all parameters data for this part"),
+                                         label=_('Copy Parameters'),
+                                         widget=forms.HiddenInput())
 
     confirm_creation = forms.BooleanField(required=False,
                                           initial=False,
@@ -142,7 +149,8 @@ class EditPartForm(HelperForm):
     class Meta:
         model = Part
         fields = [
-            'deep_copy',
+            'bom_copy',
+            'parameters_copy',
             'confirm_creation',
             'category',
             'name',
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -1041,6 +1041,7 @@ def deepCopy(self, other, **kwargs):
         Keyword Args:
             image: If True, copies Part image (default = True)
             bom: If True, copies BOM data (default = False)
+            parameters: If True, copies Parameters data (default = True)
         """
 
         # Copy the part image
@@ -1058,6 +1059,17 @@ def deepCopy(self, other, **kwargs):
                 item.pk = None
                 item.save()
 
+        # Copy the parameters data
+        if kwargs.get('parameters', True):
+            # Get template part parameters
+            parameters = other.get_parameters()
+            # Copy template part parameters to new variant part
+            for parameter in parameters:
+                PartParameter.create(part=self,
+                                     template=parameter.template,
+                                     data=parameter.data,
+                                     save=True)
+
         # Copy the fields that aren't available in the duplicate form
         self.salable = other.salable
         self.assembly = other.assembly
@@ -1402,6 +1414,13 @@ class Meta:
 
     data = models.CharField(max_length=500, help_text=_('Parameter Value'))
 
+    @classmethod
+    def create(cls, part, template, data, save=False):
+        part_parameter = cls(part=part, template=template, data=data)
+        if save:
+            part_parameter.save()
+        return part_parameter
+
 
 class BomItem(models.Model):
     """ A BomItem links a part to its component items.
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -330,7 +330,7 @@ def post(self, request, *args, **kwargs):
             data['url'] = part.get_absolute_url()
 
             # Copy relevent information from the template part
-            part.deepCopy(part_template, bom=True)
+            part.deepCopy(part_template, bom=True, parameters=True)
 
         return self.renderJsonResponse(request, form, data, context=context)
 
@@ -377,15 +377,19 @@ def get_context_data(self):
     def get_form(self):
         form = super(AjaxCreateView, self).get_form()
 
-        # Force display of the 'deep_copy' widget
-        form.fields['deep_copy'].widget = CheckboxInput()
+        # Force display of the 'bom_copy' widget
+        form.fields['bom_copy'].widget = CheckboxInput()
+
+        # Force display of the 'parameters_copy' widget
+        form.fields['parameters_copy'].widget = CheckboxInput()
 
         return form
 
     def post(self, request, *args, **kwargs):
         """ Capture the POST request for part duplication
 
-        - If the deep_copy object is set, copy all the BOM items too!
+        - If the bom_copy object is set, copy all the BOM items too!
+        - If the parameters_copy object is set, copy all the parameters too!
         """
 
         form = self.get_form()
@@ -428,12 +432,13 @@ def post(self, request, *args, **kwargs):
             data['pk'] = part.pk
             data['text'] = str(part)
 
-            deep_copy = str2bool(request.POST.get('deep_copy', False))
+            bom_copy = str2bool(request.POST.get('bom_copy', False))
+            parameters_copy = str2bool(request.POST.get('parameters_copy', False))
 
             original = self.get_part_to_copy()
 
             if original:
-                part.deepCopy(original, bom=deep_copy)
+                part.deepCopy(original, bom=bom_copy, parameters=parameters_copy)
 
             try:
                 data['url'] = part.get_absolute_url()
@@ -456,7 +461,9 @@ def get_initial(self):
         else:
             initials = super(AjaxCreateView, self).get_initial()
 
-        initials['deep_copy'] = str2bool(InvenTreeSetting.get_setting('part_deep_copy', True))
+        initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('part_deep_copy', True))
+        # Create new entry in InvenTree/common/kvp.yaml?
+        initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('part_deep_copy', True))
 
         return initials
 


Test patch: