mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Virtual part fix (#3265)
* Add 'virtual': False requirement to sales order items * Adds "virtual" filter for part table * Adds extra validation to the SalesOrderLineItem model * Prevent creation of stock items for virtual parts - Add validation check to clean() method of StockItem model - Improve validation message for StockItemSerializer class * Hide "new stock item" button for virtual parts * Hide more buttons for 'virtual' parts
This commit is contained in:
		| @@ -14,7 +14,7 @@ | ||||
|             <h4>{% trans "Supplier Parts" %}</h4> | ||||
|             {% include "spacer.html" %} | ||||
|             <div class='btn-group' role='group'> | ||||
|                 {% if roles.purchase_order.add %} | ||||
|                 {% if roles.purchase_order.add and not part.virtual %} | ||||
|                     <button class="btn btn-success" id='supplier-part-create' title='{% trans "Create new supplier part" %}'> | ||||
|                         <span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %} | ||||
|                     </button> | ||||
| @@ -61,7 +61,7 @@ | ||||
|             <h4>{% trans "Manufacturer Parts" %}</h4> | ||||
|             {% include "spacer.html" %} | ||||
|             <div class='btn-group' role='group'> | ||||
|                 {% if roles.purchase_order.add %} | ||||
|                 {% if roles.purchase_order.add and not part.virtual %} | ||||
|                     <button type="button" class="btn btn-success" id='manufacturer-part-create' title='{% trans "Create new manufacturer part" %}'> | ||||
|                         <span class='fas fa-plus-circle'></span> {% trans "New Manufacturer Part" %} | ||||
|                     </button> | ||||
|   | ||||
							
								
								
									
										26
									
								
								InvenTree/order/migrations/0071_auto_20220628_0133.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								InvenTree/order/migrations/0071_auto_20220628_0133.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| # Generated by Django 3.2.13 on 2022-06-28 01:33 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('stock', '0077_alter_stockitem_notes'), | ||||
|         ('part', '0079_alter_part_notes'), | ||||
|         ('order', '0070_auto_20220620_0728'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='salesorderallocation', | ||||
|             name='item', | ||||
|             field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'belongs_to': None, 'part__salable': True, 'part__virtual': False, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.stockitem', verbose_name='Item'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='salesorderlineitem', | ||||
|             name='part', | ||||
|             field=models.ForeignKey(help_text='Part', limit_choices_to={'salable': True, 'virtual': False}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_order_line_items', to='part.part', verbose_name='Part'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1103,6 +1103,22 @@ class SalesOrderLineItem(OrderLineItem): | ||||
|         """Return the API URL associated with the SalesOrderLineItem model""" | ||||
|         return reverse('api-so-line-list') | ||||
|  | ||||
|     def clean(self): | ||||
|         """Perform extra validation steps for this SalesOrderLineItem instance""" | ||||
|  | ||||
|         super().clean() | ||||
|  | ||||
|         if self.part: | ||||
|             if self.part.virtual: | ||||
|                 raise ValidationError({ | ||||
|                     'part': _("Virtual part cannot be assigned to a sales order") | ||||
|                 }) | ||||
|  | ||||
|             if not self.part.salable: | ||||
|                 raise ValidationError({ | ||||
|                     'part': _("Only salable parts can be assigned to a sales order") | ||||
|                 }) | ||||
|  | ||||
|     order = models.ForeignKey( | ||||
|         SalesOrder, | ||||
|         on_delete=models.CASCADE, | ||||
| @@ -1111,7 +1127,16 @@ class SalesOrderLineItem(OrderLineItem): | ||||
|         help_text=_('Sales Order') | ||||
|     ) | ||||
|  | ||||
|     part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True}) | ||||
|     part = models.ForeignKey( | ||||
|         'part.Part', on_delete=models.SET_NULL, | ||||
|         related_name='sales_order_line_items', | ||||
|         null=True, | ||||
|         verbose_name=_('Part'), | ||||
|         help_text=_('Part'), | ||||
|         limit_choices_to={ | ||||
|             'salable': True, | ||||
|             'virtual': False, | ||||
|         }) | ||||
|  | ||||
|     sale_price = InvenTreeModelMoneyField( | ||||
|         max_digits=19, | ||||
| @@ -1409,6 +1434,7 @@ class SalesOrderAllocation(models.Model): | ||||
|         related_name='sales_order_allocations', | ||||
|         limit_choices_to={ | ||||
|             'part__salable': True, | ||||
|             'part__virtual': False, | ||||
|             'belongs_to': None, | ||||
|             'sales_order': None, | ||||
|         }, | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|             <h4>{% trans "Part Stock" %}</h4> | ||||
|             {% include "spacer.html" %} | ||||
|             <div class='btn-group' role='group'> | ||||
|                 {% if roles.stock.add %} | ||||
|                 {% if roles.stock.add and not part.virtual %} | ||||
|                 <button type='button' class='btn btn-success' id='new-stock-item' title='{% trans "Create new stock item" %}'> | ||||
|                     <span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %} | ||||
|                 </button> | ||||
|   | ||||
| @@ -342,6 +342,7 @@ class StockItem(MetadataMixin, MPTTModel): | ||||
|         - Unique serial number requirement | ||||
|         - Adds a transaction note when the item is first created. | ||||
|         """ | ||||
|  | ||||
|         self.validate_unique() | ||||
|         self.clean() | ||||
|  | ||||
| @@ -439,6 +440,7 @@ class StockItem(MetadataMixin, MPTTModel): | ||||
|  | ||||
|         The following validation checks are performed: | ||||
|         - The 'part' and 'supplier_part.part' fields cannot point to the same Part object | ||||
|         - The 'part' is not virtual | ||||
|         - The 'part' does not belong to itself | ||||
|         - Quantity must be 1 if the StockItem has a serial number | ||||
|         """ | ||||
| @@ -453,12 +455,18 @@ class StockItem(MetadataMixin, MPTTModel): | ||||
|             self.batch = self.batch.strip() | ||||
|  | ||||
|         try: | ||||
|             if self.part.trackable: | ||||
|             # Trackable parts must have integer values for quantity field! | ||||
|             if self.part.trackable: | ||||
|                 if self.quantity != int(self.quantity): | ||||
|                     raise ValidationError({ | ||||
|                         'quantity': _('Quantity must be integer value for trackable parts') | ||||
|                     }) | ||||
|  | ||||
|             # Virtual parts cannot have stock items created against them | ||||
|             if self.part.virtual: | ||||
|                 raise ValidationError({ | ||||
|                     'part': _("Stock item cannot be created for virtual parts"), | ||||
|                 }) | ||||
|         except PartModels.Part.DoesNotExist: | ||||
|             # For some reason the 'clean' process sometimes throws errors because self.part does not exist | ||||
|             # It *seems* that this only occurs in unit testing, though. | ||||
| @@ -582,7 +590,8 @@ class StockItem(MetadataMixin, MPTTModel): | ||||
|     part = models.ForeignKey( | ||||
|         'part.Part', on_delete=models.CASCADE, | ||||
|         verbose_name=_('Base Part'), | ||||
|         related_name='stock_items', help_text=_('Base part'), | ||||
|         related_name='stock_items', | ||||
|         help_text=_('Base part'), | ||||
|         limit_choices_to={ | ||||
|             'virtual': False | ||||
|         }) | ||||
|   | ||||
| @@ -79,6 +79,21 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): | ||||
|     - Includes serialization for the item location | ||||
|     """ | ||||
|  | ||||
|     part = serializers.PrimaryKeyRelatedField( | ||||
|         queryset=part_models.Part.objects.all(), | ||||
|         many=False, allow_null=False, | ||||
|         help_text=_("Base Part"), | ||||
|         label=_("Part"), | ||||
|     ) | ||||
|  | ||||
|     def validate_part(self, part): | ||||
|         """Ensure the provided Part instance is valid""" | ||||
|  | ||||
|         if part.virtual: | ||||
|             raise ValidationError(_("Stock item cannot be created for virtual parts")) | ||||
|  | ||||
|         return part | ||||
|  | ||||
|     def update(self, instance, validated_data): | ||||
|         """Custom update method to pass the user information through to the instance.""" | ||||
|         instance._user = self.context['user'] | ||||
|   | ||||
| @@ -429,19 +429,27 @@ function getAvailableTableFilters(tableKey) { | ||||
|                 title: '{% trans "Include subcategories" %}', | ||||
|                 description: '{% trans "Include parts in subcategories" %}', | ||||
|             }, | ||||
|             has_ipn: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Has IPN" %}', | ||||
|                 description: '{% trans "Part has internal part number" %}', | ||||
|             }, | ||||
|             active: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Active" %}', | ||||
|                 description: '{% trans "Show active parts" %}', | ||||
|             }, | ||||
|             is_template: { | ||||
|             assembly: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Template" %}', | ||||
|                 title: '{% trans "Assembly" %}', | ||||
|             }, | ||||
|             unallocated_stock: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Available stock" %}', | ||||
|             }, | ||||
|             component: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Component" %}', | ||||
|             }, | ||||
|             has_ipn: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Has IPN" %}', | ||||
|                 description: '{% trans "Part has internal part number" %}', | ||||
|             }, | ||||
|             has_stock: { | ||||
|                 type: 'bool', | ||||
| @@ -451,34 +459,30 @@ function getAvailableTableFilters(tableKey) { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Low stock" %}', | ||||
|             }, | ||||
|             unallocated_stock: { | ||||
|             purchaseable: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Available stock" %}', | ||||
|             }, | ||||
|             assembly: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Assembly" %}', | ||||
|             }, | ||||
|             component: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Component" %}', | ||||
|             }, | ||||
|             starred: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Subscribed" %}', | ||||
|                 title: '{% trans "Purchasable" %}', | ||||
|             }, | ||||
|             salable: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Salable" %}', | ||||
|             }, | ||||
|             starred: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Subscribed" %}', | ||||
|             }, | ||||
|             is_template: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Template" %}', | ||||
|             }, | ||||
|             trackable: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Trackable" %}', | ||||
|             }, | ||||
|             purchaseable: { | ||||
|             virtual: { | ||||
|                 type: 'bool', | ||||
|                 title: '{% trans "Purchasable" %}', | ||||
|             }, | ||||
|                 title: '{% trans "Virtual" %}', | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user