2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-19 05:25:42 +00:00

Merge branch 'inventree:master' into matmair/issue2279

This commit is contained in:
Matthias Mair
2022-03-03 23:55:04 +01:00
committed by GitHub
49 changed files with 24986 additions and 23619 deletions

View File

@ -26,6 +26,8 @@ from djmoney.contrib.exchange.exceptions import MissingRate
from decimal import Decimal, InvalidOperation
from part.admin import PartResource
from .models import Part, PartCategory, PartRelated
from .models import BomItem, BomItemSubstitute
from .models import PartParameter, PartParameterTemplate
@ -43,6 +45,7 @@ from build.models import Build
from . import serializers as part_serializers
from InvenTree.helpers import str2bool, isNull, increment
from InvenTree.helpers import DownloadFile
from InvenTree.api import AttachmentMixin
from InvenTree.status_codes import BuildStatus
@ -726,6 +729,22 @@ class PartList(generics.ListCreateAPIView):
queryset = self.filter_queryset(self.get_queryset())
# Check if we wish to export the queried data to a file.
# If so, skip pagination!
export_format = request.query_params.get('export', None)
if export_format:
export_format = str(export_format).strip().lower()
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
dataset = PartResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = f"InvenTree_Parts.{export_format}"
return DownloadFile(filedata, filename)
page = self.paginate_queryset(queryset)
if page is not None:

View File

@ -1908,6 +1908,9 @@ class Part(MPTTModel):
include_inherited = kwargs.get('include_inherited', False)
# Should substitute parts be duplicated?
copy_substitutes = kwargs.get('copy_substitutes', True)
# Copy existing BOM items from another part
# Note: Inherited BOM Items will *not* be duplicated!!
for bom_item in other.get_bom_items(include_inherited=include_inherited).all():
@ -1930,11 +1933,22 @@ class Part(MPTTModel):
if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error):
continue
# Obtain a list of direct substitute parts against this BomItem
substitutes = BomItemSubstitute.objects.filter(bom_item=bom_item)
# Construct a new BOM item
bom_item.part = self
bom_item.pk = None
bom_item.save()
bom_item.refresh_from_db()
if copy_substitutes:
for sub in substitutes:
# Duplicate the substitute (and point to the *new* BomItem object)
sub.pk = None
sub.bom_item = bom_item
sub.save()
@transaction.atomic
def copy_parameters_from(self, other, **kwargs):

View File

@ -656,6 +656,9 @@ class PartCopyBOMSerializer(serializers.Serializer):
fields = [
'part',
'remove_existing',
'copy_substitutes',
'include_inherited',
'skip_invalid',
]
part = serializers.PrimaryKeyRelatedField(
@ -692,6 +695,12 @@ class PartCopyBOMSerializer(serializers.Serializer):
default=False,
)
copy_substitutes = serializers.BooleanField(
label=_('Copy Substitute Parts'),
help_text=_('Copy substitute parts when duplicate BOM items'),
default=True,
)
def save(self):
"""
Actually duplicate the BOM
@ -706,6 +715,7 @@ class PartCopyBOMSerializer(serializers.Serializer):
clear=data.get('remove_existing', True),
skip_invalid=data.get('skip_invalid', False),
include_inherited=data.get('include_inherited', False),
copy_substitutes=data.get('copy_substitutes', True),
)

View File

@ -153,9 +153,6 @@
<h4>{% trans "Parts" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
<button type='button' class='btn btn-outline-secondary' id='part-export' title='{% trans "Export Part Data" %}'>
<span class='fas fa-file-download'></span> {% trans "Export" %}
</button>
{% if roles.part.add %}
<button type='button' class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>
<span class='fas fa-plus-circle'></span> {% trans "New Part" %}
@ -290,13 +287,6 @@
});
});
$("#part-export").click(function() {
var url = "{% url 'part-export' %}?category={{ category.id }}";
location.href = url;
});
{% if roles.part.add %}
$("#part-create").click(function() {

View File

@ -28,11 +28,6 @@
</div>
</div>
<div class='panel-content'>
{% if part.is_template %}
<div class='alert alert-info alert-block'>
{% blocktrans with full_name=part.full_name%}Showing stock for all variants of <em>{{full_name}}</em>{% endblocktrans %}
</div>
{% endif %}
{% include "stock_table.html" %}
</div>
</div>
@ -281,9 +276,7 @@
</button>
<ul class='dropdown-menu' role='menu'>
<li><a class='dropdown-item' href='#' id='bom-upload'><span class='fas fa-file-upload'></span> {% trans "Upload BOM" %}</a></li>
{% if part.variant_of %}
<li><a class='dropdown-item' href='#' id='bom-duplicate'><span class='fas fa-clone'></span> {% trans "Copy BOM" %}</a></li>
{% endif %}
<li><a class='dropdown-item' href='#' id='validate-bom'><span class='fas fa-clipboard-check icon-green'></span> {% trans "Validate BOM" %}</a></li>
</ul>
</div>
@ -831,14 +824,7 @@
],
url: "{% url 'api-stock-list' %}",
});
$("#stock-export").click(function() {
exportStock({
part: {{ part.pk }}
});
});
$('#item-create').click(function () {
createNewStockItem({
data: {

View File

@ -58,14 +58,6 @@ class PartListTest(PartViewTestCase):
self.assertIn('parts', keys)
self.assertIn('user', keys)
def test_export(self):
""" Export part data to CSV """
response = self.client.get(reverse('part-export'), {'parts': '1,2,3,4,5,6,7,8,9,10'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertIn('streaming_content', dir(response))
class PartDetailTest(PartViewTestCase):

View File

@ -80,9 +80,6 @@ part_urls = [
# Download a BOM upload template
url(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='bom-upload-template'),
# Export data for multiple parts
url(r'^export/', views.PartExport.as_view(), name='part-export'),
# Individual part using pk
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),

View File

@ -49,13 +49,11 @@ from . import settings as part_settings
from .bom import MakeBomTemplate, ExportBom, IsValidBOMFormat
from order.models import PurchaseOrderLineItem
from .admin import PartResource
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView
from InvenTree.views import InvenTreeRoleMixin
from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.helpers import str2bool
class PartIndex(InvenTreeRoleMixin, ListView):
@ -709,69 +707,6 @@ class BomUpload(InvenTreeRoleMixin, DetailView):
template_name = 'part/upload_bom.html'
class PartExport(AjaxView):
""" Export a CSV file containing information on multiple parts """
role_required = 'part.view'
def get_parts(self, request):
""" Extract part list from the POST parameters.
Parts can be supplied as:
- Part category
- List of part PK values
"""
# Filter by part category
cat_id = request.GET.get('category', None)
part_list = None
if cat_id is not None:
try:
category = PartCategory.objects.get(pk=cat_id)
part_list = category.get_parts()
except (ValueError, PartCategory.DoesNotExist):
pass
# Backup - All parts
if part_list is None:
part_list = Part.objects.all()
# Also optionally filter by explicit list of part IDs
part_ids = request.GET.get('parts', '')
parts = []
for pk in part_ids.split(','):
try:
parts.append(int(pk))
except ValueError:
pass
if len(parts) > 0:
part_list = part_list.filter(pk__in=parts)
# Prefetch related fields to reduce DB hits
part_list = part_list.prefetch_related(
'category',
'used_in',
'builds',
'supplier_parts__purchase_order_line_items',
'stock_items__allocations',
)
return part_list
def get(self, request, *args, **kwargs):
parts = self.get_parts(request)
dataset = PartResource().export(queryset=parts)
csv = dataset.export('csv')
return DownloadFile(csv, 'InvenTree_Parts.csv')
class BomUploadTemplate(AjaxView):
"""
Provide a BOM upload template file for download.