mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-21 06:16:29 +00:00
Merge remote-tracking branch 'inventree/master' into build-fixes
# Conflicts: # InvenTree/InvenTree/views.py # InvenTree/build/views.py # InvenTree/locale/de/LC_MESSAGES/django.po # InvenTree/locale/en/LC_MESSAGES/django.po # InvenTree/locale/es/LC_MESSAGES/django.po # InvenTree/order/views.py # InvenTree/part/api.py # InvenTree/part/views.py # InvenTree/templates/js/bom.js
This commit is contained in:
@ -786,12 +786,11 @@ class BomList(generics.ListCreateAPIView):
|
||||
validated = params.get('validated', None)
|
||||
|
||||
if validated is not None:
|
||||
|
||||
validated = str2bool(validated)
|
||||
|
||||
# Work out which lines have actually been validated
|
||||
pks = []
|
||||
|
||||
|
||||
for bom_item in queryset.all():
|
||||
if bom_item.is_line_valid:
|
||||
pks.append(bom_item.pk)
|
||||
|
@ -19,10 +19,15 @@ from .models import PartParameterTemplate, PartParameter
|
||||
from .models import PartTestTemplate
|
||||
from .models import PartSellPriceBreak
|
||||
|
||||
|
||||
from common.models import Currency
|
||||
|
||||
|
||||
class PartModelChoiceField(forms.ModelChoiceField):
|
||||
""" Extending string representation of Part instance with available stock """
|
||||
def label_from_instance(self, part):
|
||||
return f'{part} - {part.available_stock}'
|
||||
|
||||
|
||||
class PartImageForm(HelperForm):
|
||||
""" Form for uploading a Part image """
|
||||
|
||||
@ -77,6 +82,38 @@ class BomExportForm(forms.Form):
|
||||
self.fields['file_format'].choices = self.get_choices()
|
||||
|
||||
|
||||
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,
|
||||
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,
|
||||
@ -210,12 +247,6 @@ class EditCategoryForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class PartModelChoiceField(forms.ModelChoiceField):
|
||||
""" Extending string representation of Part instance with available stock """
|
||||
def label_from_instance(self, part):
|
||||
return f'{part} - {part.available_stock}'
|
||||
|
||||
|
||||
class EditBomItemForm(HelperForm):
|
||||
""" Form for editing a BomItem object """
|
||||
|
||||
|
@ -1134,6 +1134,60 @@ class Part(MPTTModel):
|
||||
max(buy_price_range[1], bom_price_range[1])
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def copy_bom_from(self, other, clear=True, **kwargs):
|
||||
"""
|
||||
Copy the BOM from another part.
|
||||
|
||||
args:
|
||||
other - The part to copy the BOM from
|
||||
clear - Remove existing BOM items first (default=True)
|
||||
"""
|
||||
|
||||
if clear:
|
||||
# Remove existing BOM items
|
||||
self.bom_items.all().delete()
|
||||
|
||||
for bom_item in other.bom_items.all():
|
||||
# If this part already has a BomItem pointing to the same sub-part,
|
||||
# delete that BomItem from this part first!
|
||||
|
||||
try:
|
||||
existing = BomItem.objects.get(part=self, sub_part=bom_item.sub_part)
|
||||
existing.delete()
|
||||
except (BomItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
bom_item.part = self
|
||||
bom_item.pk = None
|
||||
|
||||
bom_item.save()
|
||||
|
||||
@transaction.atomic
|
||||
def copy_parameters_from(self, other, **kwargs):
|
||||
|
||||
clear = kwargs.get('clear', True)
|
||||
|
||||
if clear:
|
||||
self.get_parameters().delete()
|
||||
|
||||
for parameter in other.get_parameters():
|
||||
|
||||
# If this part already has a parameter pointing to the same template,
|
||||
# delete that parameter from this part first!
|
||||
|
||||
try:
|
||||
existing = PartParameter.objects.get(part=self, template=parameter.template)
|
||||
existing.delete()
|
||||
except (PartParameter.DoesNotExist):
|
||||
pass
|
||||
|
||||
parameter.part = self
|
||||
parameter.pk = None
|
||||
|
||||
parameter.save()
|
||||
|
||||
@transaction.atomic
|
||||
def deepCopy(self, other, **kwargs):
|
||||
""" Duplicates non-field data from another part.
|
||||
Does not alter the normal fields of this part,
|
||||
@ -1153,24 +1207,12 @@ class Part(MPTTModel):
|
||||
|
||||
# Copy the BOM data
|
||||
if kwargs.get('bom', False):
|
||||
for item in other.bom_items.all():
|
||||
# Point the item to THIS part.
|
||||
# Set the pk to None so a new entry is created.
|
||||
item.part = self
|
||||
item.pk = None
|
||||
item.save()
|
||||
self.copy_bom_from(other)
|
||||
|
||||
# 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)
|
||||
|
||||
self.copy_parameters_from(other)
|
||||
|
||||
# Copy the fields that aren't available in the duplicate form
|
||||
self.salable = other.salable
|
||||
self.assembly = other.assembly
|
||||
|
@ -39,8 +39,13 @@
|
||||
<span class='fas fa-trash-alt'></span>
|
||||
</button>
|
||||
<button class='btn btn-primary' type='button' title='{% trans "Import BOM data" %}' id='bom-upload'>
|
||||
<span class='fas fa-file-upload'></span> {% trans "Upload" %}
|
||||
<span class='fas fa-file-upload'></span> {% trans "Import from File" %}
|
||||
</button>
|
||||
{% if part.variant_of %}
|
||||
<button class='btn btn-default' type='button' title='{% trans "Copy BOM from parent part" %}' id='bom-duplicate'>
|
||||
<span class='fas fa-clone'></span> {% trans "Copy from Parent" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class='btn btn-default' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Add Item" %}
|
||||
</button>
|
||||
@ -157,6 +162,17 @@
|
||||
location.href = "{% url 'upload-bom' part.id %}";
|
||||
});
|
||||
|
||||
$('#bom-duplicate').click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'duplicate-bom' part.id %}",
|
||||
{
|
||||
success: function() {
|
||||
$('#bom-table').bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#bom-item-new").click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'bom-item-create' %}?parent={{ part.id }}",
|
||||
|
17
InvenTree/part/templates/part/bom_duplicate.html
Normal file
17
InvenTree/part/templates/part/bom_duplicate.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% 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'>
|
||||
<b>{% trans "Warning" %}</b><br>
|
||||
{% trans "This part already has a Bill of Materials" %}<br>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -46,6 +46,7 @@ part_detail_urls = [
|
||||
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
|
||||
|
||||
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
|
||||
url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'),
|
||||
|
||||
url(r'^params/', views.PartDetail.as_view(template_name='part/params.html'), name='part-params'),
|
||||
url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'),
|
||||
|
@ -371,6 +371,8 @@ class MakePartVariant(AjaxCreateView):
|
||||
initials = model_to_dict(part_template)
|
||||
initials['is_template'] = False
|
||||
initials['variant_of'] = part_template
|
||||
initials['bom_copy'] = InvenTreeSetting.get_setting('PART_COPY_BOM')
|
||||
initials['parameters_copy'] = InvenTreeSetting.get_setting('PART_COPY_PARAMETERS')
|
||||
|
||||
return initials
|
||||
|
||||
@ -832,8 +834,60 @@ class PartEdit(AjaxUpdateView):
|
||||
return form
|
||||
|
||||
|
||||
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
|
||||
role_required = 'part.change'
|
||||
|
||||
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 post_save(self, part, form):
|
||||
|
||||
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 """
|
||||
"""
|
||||
Modal form view for validating a part BOM
|
||||
"""
|
||||
|
||||
model = Part
|
||||
ajax_form_title = _("Validate BOM")
|
||||
@ -854,23 +908,21 @@ class BomValidate(AjaxUpdateView):
|
||||
|
||||
return self.renderJsonResponse(request, form, context=self.get_context())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
def validate(self, part, form, **kwargs):
|
||||
|
||||
form = self.get_form()
|
||||
part = self.get_object()
|
||||
confirm = str2bool(form.cleaned_data.get('validate', False))
|
||||
|
||||
confirmed = str2bool(request.POST.get('validate', False))
|
||||
|
||||
if confirmed:
|
||||
part.validate_bom(request.user)
|
||||
else:
|
||||
if not confirm:
|
||||
form.add_error('validate', _('Confirm that the BOM is valid'))
|
||||
|
||||
data = {
|
||||
'form_valid': confirmed
|
||||
}
|
||||
def post_save(self, part, form, **kwargs):
|
||||
|
||||
return self.renderJsonResponse(request, form, data, context=self.get_context())
|
||||
part.validate_bom(self.request.user)
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'success': _('Validated Bill of Materials')
|
||||
}
|
||||
|
||||
|
||||
class BomUpload(InvenTreeRoleMixin, FormView):
|
||||
|
Reference in New Issue
Block a user