mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Merge pull request #2477 from SchrodingersGat/duplicate-bom-form
Duplicate bom form
This commit is contained in:
commit
774d834b7e
@ -454,6 +454,76 @@ class PartSerialNumberDetail(generics.RetrieveAPIView):
|
|||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
class PartCopyBOM(generics.CreateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for duplicating a BOM
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = Part.objects.all()
|
||||||
|
serializer_class = part_serializers.PartCopyBOMSerializer
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx['part'] = Part.objects.get(pk=self.kwargs.get('pk', None))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class PartValidateBOM(generics.RetrieveUpdateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for 'validating' the BOM for a given Part
|
||||||
|
"""
|
||||||
|
|
||||||
|
class BOMValidateSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Part
|
||||||
|
fields = [
|
||||||
|
'checksum',
|
||||||
|
'valid',
|
||||||
|
]
|
||||||
|
|
||||||
|
checksum = serializers.CharField(
|
||||||
|
read_only=True,
|
||||||
|
source='bom_checksum',
|
||||||
|
)
|
||||||
|
|
||||||
|
valid = serializers.BooleanField(
|
||||||
|
write_only=True,
|
||||||
|
default=False,
|
||||||
|
label=_('Valid'),
|
||||||
|
help_text=_('Validate entire Bill of Materials'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_valid(self, valid):
|
||||||
|
if not valid:
|
||||||
|
raise ValidationError(_('This option must be selected'))
|
||||||
|
|
||||||
|
queryset = Part.objects.all()
|
||||||
|
|
||||||
|
serializer_class = BOMValidateSerializer
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
part = self.get_object()
|
||||||
|
|
||||||
|
partial = kwargs.pop('partial', False)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(part, data=request.data, partial=partial)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
part.validate_bom(request.user)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'checksum': part.bom_checksum,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
""" API endpoint for detail view of a single Part object """
|
""" API endpoint for detail view of a single Part object """
|
||||||
|
|
||||||
@ -1585,6 +1655,12 @@ part_api_urls = [
|
|||||||
# Endpoint for extra serial number information
|
# Endpoint for extra serial number information
|
||||||
url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'),
|
url(r'^serial-numbers/', PartSerialNumberDetail.as_view(), name='api-part-serial-number-detail'),
|
||||||
|
|
||||||
|
# Endpoint for duplicating a BOM for the specific Part
|
||||||
|
url(r'^bom-copy/', PartCopyBOM.as_view(), name='api-part-bom-copy'),
|
||||||
|
|
||||||
|
# Endpoint for validating a BOM for the specific Part
|
||||||
|
url(r'^bom-validate/', PartValidateBOM.as_view(), name='api-part-bom-validate'),
|
||||||
|
|
||||||
# Part detail endpoint
|
# Part detail endpoint
|
||||||
url(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
|
url(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
|
||||||
])),
|
])),
|
||||||
|
@ -55,54 +55,6 @@ class PartImageDownloadForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BomDuplicateForm(HelperForm):
|
|
||||||
"""
|
|
||||||
Simple confirmation form for BOM duplication.
|
|
||||||
|
|
||||||
Select which parent to select from.
|
|
||||||
"""
|
|
||||||
|
|
||||||
parent = PartModelChoiceField(
|
|
||||||
label=_('Parent Part'),
|
|
||||||
help_text=_('Select parent part to copy BOM from'),
|
|
||||||
queryset=Part.objects.filter(is_template=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
clear = forms.BooleanField(
|
|
||||||
required=False, initial=True,
|
|
||||||
help_text=_('Clear existing BOM items')
|
|
||||||
)
|
|
||||||
|
|
||||||
confirm = forms.BooleanField(
|
|
||||||
required=False, initial=False,
|
|
||||||
label=_('Confirm'),
|
|
||||||
help_text=_('Confirm BOM duplication')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Part
|
|
||||||
fields = [
|
|
||||||
'parent',
|
|
||||||
'clear',
|
|
||||||
'confirm',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BomValidateForm(HelperForm):
|
|
||||||
""" Simple confirmation form for BOM validation.
|
|
||||||
User is presented with a single checkbox input,
|
|
||||||
to confirm that the BOM for this part is valid
|
|
||||||
"""
|
|
||||||
|
|
||||||
validate = forms.BooleanField(required=False, initial=False, label=_('validate'), help_text=_('Confirm that the BOM is correct'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Part
|
|
||||||
fields = [
|
|
||||||
'validate'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BomMatchItemForm(MatchItemForm):
|
class BomMatchItemForm(MatchItemForm):
|
||||||
""" Override MatchItemForm fields """
|
""" Override MatchItemForm fields """
|
||||||
|
|
||||||
|
@ -481,7 +481,7 @@ class Part(MPTTModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.full_name} - {self.description}"
|
return f"{self.full_name} - {self.description}"
|
||||||
|
|
||||||
def checkAddToBOM(self, parent):
|
def check_add_to_bom(self, parent, raise_error=False, recursive=True):
|
||||||
"""
|
"""
|
||||||
Check if this Part can be added to the BOM of another part.
|
Check if this Part can be added to the BOM of another part.
|
||||||
|
|
||||||
@ -491,33 +491,44 @@ class Part(MPTTModel):
|
|||||||
b) The parent part is used in the BOM for *this* part
|
b) The parent part is used in the BOM for *this* part
|
||||||
c) The parent part is used in the BOM for any child parts under this one
|
c) The parent part is used in the BOM for any child parts under this one
|
||||||
|
|
||||||
Failing this check raises a ValidationError!
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if parent is None:
|
result = True
|
||||||
return
|
|
||||||
|
|
||||||
if self.pk == parent.pk:
|
try:
|
||||||
raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format(
|
if self.pk == parent.pk:
|
||||||
p1=str(self),
|
|
||||||
p2=str(parent)
|
|
||||||
)})
|
|
||||||
|
|
||||||
bom_items = self.get_bom_items()
|
|
||||||
|
|
||||||
# Ensure that the parent part does not appear under any child BOM item!
|
|
||||||
for item in bom_items.all():
|
|
||||||
|
|
||||||
# Check for simple match
|
|
||||||
if item.sub_part == parent:
|
|
||||||
raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format(
|
raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format(
|
||||||
p1=str(parent),
|
p1=str(self),
|
||||||
p2=str(self)
|
p2=str(parent)
|
||||||
)})
|
)})
|
||||||
|
|
||||||
# And recursively check too
|
bom_items = self.get_bom_items()
|
||||||
item.sub_part.checkAddToBOM(parent)
|
|
||||||
|
# Ensure that the parent part does not appear under any child BOM item!
|
||||||
|
for item in bom_items.all():
|
||||||
|
|
||||||
|
# Check for simple match
|
||||||
|
if item.sub_part == parent:
|
||||||
|
raise ValidationError({'sub_part': _("Part '{p1}' is used in BOM for '{p2}' (recursive)").format(
|
||||||
|
p1=str(parent),
|
||||||
|
p2=str(self)
|
||||||
|
)})
|
||||||
|
|
||||||
|
# And recursively check too
|
||||||
|
if recursive:
|
||||||
|
result = result and item.sub_part.check_add_to_bom(
|
||||||
|
parent,
|
||||||
|
recursive=True,
|
||||||
|
raise_error=raise_error
|
||||||
|
)
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
if raise_error:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def checkIfSerialNumberExists(self, sn, exclude_self=False):
|
def checkIfSerialNumberExists(self, sn, exclude_self=False):
|
||||||
"""
|
"""
|
||||||
@ -1816,23 +1827,45 @@ class Part(MPTTModel):
|
|||||||
clear - Remove existing BOM items first (default=True)
|
clear - Remove existing BOM items first (default=True)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Ignore if the other part is actually this part?
|
||||||
|
if other == self:
|
||||||
|
return
|
||||||
|
|
||||||
if clear:
|
if clear:
|
||||||
# Remove existing BOM items
|
# Remove existing BOM items
|
||||||
# Note: Inherited BOM items are *not* deleted!
|
# Note: Inherited BOM items are *not* deleted!
|
||||||
self.bom_items.all().delete()
|
self.bom_items.all().delete()
|
||||||
|
|
||||||
|
# List of "ancestor" parts above this one
|
||||||
|
my_ancestors = self.get_ancestors(include_self=False)
|
||||||
|
|
||||||
|
raise_error = not kwargs.get('skip_invalid', True)
|
||||||
|
|
||||||
|
include_inherited = kwargs.get('include_inherited', False)
|
||||||
|
|
||||||
# Copy existing BOM items from another part
|
# Copy existing BOM items from another part
|
||||||
# Note: Inherited BOM Items will *not* be duplicated!!
|
# Note: Inherited BOM Items will *not* be duplicated!!
|
||||||
for bom_item in other.get_bom_items(include_inherited=False).all():
|
for bom_item in other.get_bom_items(include_inherited=include_inherited).all():
|
||||||
# If this part already has a BomItem pointing to the same sub-part,
|
# If this part already has a BomItem pointing to the same sub-part,
|
||||||
# delete that BomItem from this part first!
|
# delete that BomItem from this part first!
|
||||||
|
|
||||||
try:
|
# Ignore invalid BomItem objects
|
||||||
existing = BomItem.objects.get(part=self, sub_part=bom_item.sub_part)
|
if not bom_item.part or not bom_item.sub_part:
|
||||||
existing.delete()
|
continue
|
||||||
except (BomItem.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
# Ignore ancestor parts which are inherited
|
||||||
|
if bom_item.part in my_ancestors and bom_item.inherited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip if already exists
|
||||||
|
if BomItem.objects.filter(part=self, sub_part=bom_item.sub_part).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip (or throw error) if BomItem is not valid
|
||||||
|
if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Construct a new BOM item
|
||||||
bom_item.part = self
|
bom_item.part = self
|
||||||
bom_item.pk = None
|
bom_item.pk = None
|
||||||
|
|
||||||
@ -2677,7 +2710,7 @@ class BomItem(models.Model):
|
|||||||
try:
|
try:
|
||||||
# Check for circular BOM references
|
# Check for circular BOM references
|
||||||
if self.sub_part:
|
if self.sub_part:
|
||||||
self.sub_part.checkAddToBOM(self.part)
|
self.sub_part.check_add_to_bom(self.part, raise_error=True)
|
||||||
|
|
||||||
# If the sub_part is 'trackable' then the 'quantity' field must be an integer
|
# If the sub_part is 'trackable' then the 'quantity' field must be an integer
|
||||||
if self.sub_part.trackable:
|
if self.sub_part.trackable:
|
||||||
|
@ -9,6 +9,7 @@ from django.urls import reverse_lazy
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from sql_util.utils import SubqueryCount, SubquerySum
|
from sql_util.utils import SubqueryCount, SubquerySum
|
||||||
@ -636,3 +637,65 @@ class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
|
|||||||
'parameter_template',
|
'parameter_template',
|
||||||
'default_value',
|
'default_value',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartCopyBOMSerializer(serializers.Serializer):
|
||||||
|
"""
|
||||||
|
Serializer for copying a BOM from another part
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'part',
|
||||||
|
'remove_existing',
|
||||||
|
]
|
||||||
|
|
||||||
|
part = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=Part.objects.all(),
|
||||||
|
many=False,
|
||||||
|
required=True,
|
||||||
|
allow_null=False,
|
||||||
|
label=_('Part'),
|
||||||
|
help_text=_('Select part to copy BOM from'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_part(self, part):
|
||||||
|
"""
|
||||||
|
Check that a 'valid' part was selected
|
||||||
|
"""
|
||||||
|
|
||||||
|
return part
|
||||||
|
|
||||||
|
remove_existing = serializers.BooleanField(
|
||||||
|
label=_('Remove Existing Data'),
|
||||||
|
help_text=_('Remove existing BOM items before copying'),
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
include_inherited = serializers.BooleanField(
|
||||||
|
label=_('Include Inherited'),
|
||||||
|
help_text=_('Include BOM items which are inherited from templated parts'),
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
skip_invalid = serializers.BooleanField(
|
||||||
|
label=_('Skip Invalid Rows'),
|
||||||
|
help_text=_('Enable this option to skip invalid rows'),
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""
|
||||||
|
Actually duplicate the BOM
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_part = self.context['part']
|
||||||
|
|
||||||
|
data = self.validated_data
|
||||||
|
|
||||||
|
base_part.copy_bom_from(
|
||||||
|
data['part'],
|
||||||
|
clear=data.get('remove_existing', True),
|
||||||
|
skip_invalid=data.get('skip_invalid', False),
|
||||||
|
include_inherited=data.get('include_inherited', False),
|
||||||
|
)
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
{% extends "modal_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block pre_form_content %}
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{% trans "Select parent part to copy BOM from" %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% if part.has_bom %}
|
|
||||||
<div class='alert alert-block alert-danger'>
|
|
||||||
<strong>{% trans "Warning" %}</strong><br>
|
|
||||||
{% trans "This part already has a Bill of Materials" %}<br>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,12 +0,0 @@
|
|||||||
{% extends "modal_form.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block pre_form_content %}
|
|
||||||
{% blocktrans with part.full_name as part %}Confirm that the Bill of Materials (BOM) is valid for:<br><em>{{ part }}</em>{% endblocktrans %}
|
|
||||||
|
|
||||||
<div class='alert alert-warning alert-block'>
|
|
||||||
{% trans 'This will validate each line in the BOM.' %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -585,14 +585,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#bom-duplicate').click(function() {
|
$('#bom-duplicate').click(function() {
|
||||||
launchModalForm(
|
|
||||||
"{% url 'duplicate-bom' part.id %}",
|
duplicateBom({{ part.pk }}, {
|
||||||
{
|
success: function(response) {
|
||||||
success: function() {
|
$('#bom-table').bootstrapTable('refresh');
|
||||||
$('#bom-table').bootstrapTable('refresh');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#bom-item-new").click(function () {
|
$("#bom-item-new").click(function () {
|
||||||
@ -616,12 +614,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#validate-bom").click(function() {
|
$("#validate-bom").click(function() {
|
||||||
launchModalForm(
|
|
||||||
"{% url 'bom-validate' part.id %}",
|
validateBom({{ part.id }}, {
|
||||||
{
|
reload: true
|
||||||
reload: true,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#download-bom").click(function () {
|
$("#download-bom").click(function () {
|
||||||
|
@ -35,12 +35,10 @@ part_detail_urls = [
|
|||||||
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
|
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
|
||||||
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
|
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
|
||||||
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
|
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
|
||||||
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
|
|
||||||
|
|
||||||
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
|
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
|
||||||
|
|
||||||
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
|
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
|
||||||
url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'),
|
|
||||||
|
|
||||||
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
|
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
|
||||||
|
|
||||||
|
@ -694,100 +694,6 @@ class PartImageSelect(AjaxUpdateView):
|
|||||||
return self.renderJsonResponse(request, form, data)
|
return self.renderJsonResponse(request, form, data)
|
||||||
|
|
||||||
|
|
||||||
class BomDuplicate(AjaxUpdateView):
|
|
||||||
"""
|
|
||||||
View for duplicating BOM from a parent item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Part
|
|
||||||
context_object_name = 'part'
|
|
||||||
ajax_form_title = _('Duplicate BOM')
|
|
||||||
ajax_template_name = 'part/bom_duplicate.html'
|
|
||||||
form_class = part_forms.BomDuplicateForm
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
|
|
||||||
form = super().get_form()
|
|
||||||
|
|
||||||
# Limit choices to parents of the current part
|
|
||||||
parents = self.get_object().get_ancestors()
|
|
||||||
|
|
||||||
form.fields['parent'].queryset = parents
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
initials = super().get_initial()
|
|
||||||
|
|
||||||
parents = self.get_object().get_ancestors()
|
|
||||||
|
|
||||||
if parents.count() == 1:
|
|
||||||
initials['parent'] = parents[0]
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
def validate(self, part, form):
|
|
||||||
|
|
||||||
confirm = str2bool(form.cleaned_data.get('confirm', False))
|
|
||||||
|
|
||||||
if not confirm:
|
|
||||||
form.add_error('confirm', _('Confirm duplication of BOM from parent'))
|
|
||||||
|
|
||||||
def save(self, part, form):
|
|
||||||
"""
|
|
||||||
Duplicate BOM from the specified parent
|
|
||||||
"""
|
|
||||||
|
|
||||||
parent = form.cleaned_data.get('parent', None)
|
|
||||||
|
|
||||||
clear = str2bool(form.cleaned_data.get('clear', True))
|
|
||||||
|
|
||||||
if parent:
|
|
||||||
part.copy_bom_from(parent, clear=clear)
|
|
||||||
|
|
||||||
|
|
||||||
class BomValidate(AjaxUpdateView):
|
|
||||||
"""
|
|
||||||
Modal form view for validating a part BOM
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Part
|
|
||||||
ajax_form_title = _("Validate BOM")
|
|
||||||
ajax_template_name = 'part/bom_validate.html'
|
|
||||||
context_object_name = 'part'
|
|
||||||
form_class = part_forms.BomValidateForm
|
|
||||||
|
|
||||||
def get_context(self):
|
|
||||||
return {
|
|
||||||
'part': self.get_object(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
|
|
||||||
form = self.get_form()
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, context=self.get_context())
|
|
||||||
|
|
||||||
def validate(self, part, form, **kwargs):
|
|
||||||
|
|
||||||
confirm = str2bool(form.cleaned_data.get('validate', False))
|
|
||||||
|
|
||||||
if not confirm:
|
|
||||||
form.add_error('validate', _('Confirm that the BOM is valid'))
|
|
||||||
|
|
||||||
def save(self, part, form, **kwargs):
|
|
||||||
"""
|
|
||||||
Mark the BOM as validated
|
|
||||||
"""
|
|
||||||
|
|
||||||
part.validate_bom(self.request.user)
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return {
|
|
||||||
'success': _('Validated Bill of Materials')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BomUpload(InvenTreeRoleMixin, FileManagementFormView):
|
class BomUpload(InvenTreeRoleMixin, FileManagementFormView):
|
||||||
""" View for uploading a BOM file, and handling BOM data importing.
|
""" View for uploading a BOM file, and handling BOM data importing.
|
||||||
|
|
||||||
|
@ -207,6 +207,11 @@ function showApiError(xhr, url) {
|
|||||||
title = '{% trans "Error 404: Resource Not Found" %}';
|
title = '{% trans "Error 404: Resource Not Found" %}';
|
||||||
message = '{% trans "The requested resource could not be located on the server" %}';
|
message = '{% trans "The requested resource could not be located on the server" %}';
|
||||||
break;
|
break;
|
||||||
|
// Method not allowed
|
||||||
|
case 405:
|
||||||
|
title = '{% trans "Error 405: Method Not Allowed" %}';
|
||||||
|
message = '{% trans "HTTP method not allowed at URL" %}';
|
||||||
|
break;
|
||||||
// Timeout
|
// Timeout
|
||||||
case 408:
|
case 408:
|
||||||
title = '{% trans "Error 408: Timeout" %}';
|
title = '{% trans "Error 408: Timeout" %}';
|
||||||
|
@ -661,7 +661,7 @@ function loadBomTable(table, options={}) {
|
|||||||
if (!row.inherited) {
|
if (!row.inherited) {
|
||||||
return yesNoLabel(false);
|
return yesNoLabel(false);
|
||||||
} else if (row.part == options.parent_id) {
|
} else if (row.part == options.parent_id) {
|
||||||
return '{% trans "Inherited" %}';
|
return yesNoLabel(true);
|
||||||
} else {
|
} else {
|
||||||
// If this BOM item is inherited from a parent part
|
// If this BOM item is inherited from a parent part
|
||||||
return renderLink(
|
return renderLink(
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
|
duplicateBom,
|
||||||
duplicatePart,
|
duplicatePart,
|
||||||
editCategory,
|
editCategory,
|
||||||
editPart,
|
editPart,
|
||||||
@ -39,6 +40,7 @@
|
|||||||
loadStockPricingChart,
|
loadStockPricingChart,
|
||||||
partStockLabel,
|
partStockLabel,
|
||||||
toggleStar,
|
toggleStar,
|
||||||
|
validateBom,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Part API functions
|
/* Part API functions
|
||||||
@ -428,6 +430,59 @@ function toggleStar(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Validate a BOM */
|
||||||
|
function validateBom(part_id, options={}) {
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<div class='alert alert-block alert-success'>
|
||||||
|
{% trans "Validating the BOM will mark each line item as valid" %}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
constructForm(`/api/part/${part_id}/bom-validate/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
fields: {
|
||||||
|
valid: {},
|
||||||
|
},
|
||||||
|
preFormContent: html,
|
||||||
|
title: '{% trans "Validate Bill of Materials" %}',
|
||||||
|
reload: options.reload,
|
||||||
|
onSuccess: function(response) {
|
||||||
|
showMessage('{% trans "Validated Bill of Materials" %}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Duplicate a BOM */
|
||||||
|
function duplicateBom(part_id, options={}) {
|
||||||
|
|
||||||
|
constructForm(`/api/part/${part_id}/bom-copy/`, {
|
||||||
|
method: 'POST',
|
||||||
|
fields: {
|
||||||
|
part: {
|
||||||
|
icon: 'fa-shapes',
|
||||||
|
filters: {
|
||||||
|
assembly: true,
|
||||||
|
exclude_tree: part_id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include_inherited: {},
|
||||||
|
remove_existing: {},
|
||||||
|
skip_invalid: {},
|
||||||
|
},
|
||||||
|
confirm: true,
|
||||||
|
title: '{% trans "Copy Bill of Materials" %}',
|
||||||
|
onSuccess: function(response) {
|
||||||
|
if (options.success) {
|
||||||
|
options.success(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function partStockLabel(part, options={}) {
|
function partStockLabel(part, options={}) {
|
||||||
|
|
||||||
if (part.in_stock) {
|
if (part.in_stock) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user