mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +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:
parent
b63ba4b636
commit
8f92fddd2d
@ -14,7 +14,7 @@
|
|||||||
<h4>{% trans "Supplier Parts" %}</h4>
|
<h4>{% trans "Supplier Parts" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<div class='btn-group' role='group'>
|
<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" %}'>
|
<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" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
||||||
</button>
|
</button>
|
||||||
@ -61,7 +61,7 @@
|
|||||||
<h4>{% trans "Manufacturer Parts" %}</h4>
|
<h4>{% trans "Manufacturer Parts" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<div class='btn-group' role='group'>
|
<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" %}'>
|
<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" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Manufacturer Part" %}
|
||||||
</button>
|
</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 the API URL associated with the SalesOrderLineItem model"""
|
||||||
return reverse('api-so-line-list')
|
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(
|
order = models.ForeignKey(
|
||||||
SalesOrder,
|
SalesOrder,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -1111,7 +1127,16 @@ class SalesOrderLineItem(OrderLineItem):
|
|||||||
help_text=_('Sales Order')
|
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(
|
sale_price = InvenTreeModelMoneyField(
|
||||||
max_digits=19,
|
max_digits=19,
|
||||||
@ -1409,6 +1434,7 @@ class SalesOrderAllocation(models.Model):
|
|||||||
related_name='sales_order_allocations',
|
related_name='sales_order_allocations',
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
'part__salable': True,
|
'part__salable': True,
|
||||||
|
'part__virtual': False,
|
||||||
'belongs_to': None,
|
'belongs_to': None,
|
||||||
'sales_order': None,
|
'sales_order': None,
|
||||||
},
|
},
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<h4>{% trans "Part Stock" %}</h4>
|
<h4>{% trans "Part Stock" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<div class='btn-group' role='group'>
|
<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" %}'>
|
<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" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -342,6 +342,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
|||||||
- Unique serial number requirement
|
- Unique serial number requirement
|
||||||
- Adds a transaction note when the item is first created.
|
- Adds a transaction note when the item is first created.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.validate_unique()
|
self.validate_unique()
|
||||||
self.clean()
|
self.clean()
|
||||||
|
|
||||||
@ -439,6 +440,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
|||||||
|
|
||||||
The following validation checks are performed:
|
The following validation checks are performed:
|
||||||
- The 'part' and 'supplier_part.part' fields cannot point to the same Part object
|
- 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
|
- The 'part' does not belong to itself
|
||||||
- Quantity must be 1 if the StockItem has a serial number
|
- Quantity must be 1 if the StockItem has a serial number
|
||||||
"""
|
"""
|
||||||
@ -453,12 +455,18 @@ class StockItem(MetadataMixin, MPTTModel):
|
|||||||
self.batch = self.batch.strip()
|
self.batch = self.batch.strip()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Trackable parts must have integer values for quantity field!
|
||||||
if self.part.trackable:
|
if self.part.trackable:
|
||||||
# Trackable parts must have integer values for quantity field!
|
|
||||||
if self.quantity != int(self.quantity):
|
if self.quantity != int(self.quantity):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'quantity': _('Quantity must be integer value for trackable parts')
|
'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:
|
except PartModels.Part.DoesNotExist:
|
||||||
# For some reason the 'clean' process sometimes throws errors because self.part does not exist
|
# 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.
|
# It *seems* that this only occurs in unit testing, though.
|
||||||
@ -582,7 +590,8 @@ class StockItem(MetadataMixin, MPTTModel):
|
|||||||
part = models.ForeignKey(
|
part = models.ForeignKey(
|
||||||
'part.Part', on_delete=models.CASCADE,
|
'part.Part', on_delete=models.CASCADE,
|
||||||
verbose_name=_('Base Part'),
|
verbose_name=_('Base Part'),
|
||||||
related_name='stock_items', help_text=_('Base part'),
|
related_name='stock_items',
|
||||||
|
help_text=_('Base part'),
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
'virtual': False
|
'virtual': False
|
||||||
})
|
})
|
||||||
|
@ -79,6 +79,21 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
- Includes serialization for the item location
|
- 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):
|
def update(self, instance, validated_data):
|
||||||
"""Custom update method to pass the user information through to the instance."""
|
"""Custom update method to pass the user information through to the instance."""
|
||||||
instance._user = self.context['user']
|
instance._user = self.context['user']
|
||||||
|
@ -429,19 +429,27 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
title: '{% trans "Include subcategories" %}',
|
title: '{% trans "Include subcategories" %}',
|
||||||
description: '{% trans "Include parts in subcategories" %}',
|
description: '{% trans "Include parts in subcategories" %}',
|
||||||
},
|
},
|
||||||
has_ipn: {
|
|
||||||
type: 'bool',
|
|
||||||
title: '{% trans "Has IPN" %}',
|
|
||||||
description: '{% trans "Part has internal part number" %}',
|
|
||||||
},
|
|
||||||
active: {
|
active: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Active" %}',
|
title: '{% trans "Active" %}',
|
||||||
description: '{% trans "Show active parts" %}',
|
description: '{% trans "Show active parts" %}',
|
||||||
},
|
},
|
||||||
is_template: {
|
assembly: {
|
||||||
type: 'bool',
|
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: {
|
has_stock: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
@ -451,34 +459,30 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Low stock" %}',
|
title: '{% trans "Low stock" %}',
|
||||||
},
|
},
|
||||||
unallocated_stock: {
|
purchaseable: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Available stock" %}',
|
title: '{% trans "Purchasable" %}',
|
||||||
},
|
|
||||||
assembly: {
|
|
||||||
type: 'bool',
|
|
||||||
title: '{% trans "Assembly" %}',
|
|
||||||
},
|
|
||||||
component: {
|
|
||||||
type: 'bool',
|
|
||||||
title: '{% trans "Component" %}',
|
|
||||||
},
|
|
||||||
starred: {
|
|
||||||
type: 'bool',
|
|
||||||
title: '{% trans "Subscribed" %}',
|
|
||||||
},
|
},
|
||||||
salable: {
|
salable: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Salable" %}',
|
title: '{% trans "Salable" %}',
|
||||||
},
|
},
|
||||||
|
starred: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Subscribed" %}',
|
||||||
|
},
|
||||||
|
is_template: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Template" %}',
|
||||||
|
},
|
||||||
trackable: {
|
trackable: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Trackable" %}',
|
title: '{% trans "Trackable" %}',
|
||||||
},
|
},
|
||||||
purchaseable: {
|
virtual: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Purchasable" %}',
|
title: '{% trans "Virtual" %}',
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user