2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-08 04:40:57 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into plugin-2037

This commit is contained in:
Matthias
2021-10-08 22:21:11 +02:00
74 changed files with 3305 additions and 2378 deletions

View File

@@ -1100,6 +1100,12 @@ class BomList(generics.ListCreateAPIView):
except AttributeError:
pass
try:
# Include or exclude pricing information in the serialized data
kwargs['include_pricing'] = str2bool(self.request.GET.get('include_pricing', True))
except AttributeError:
pass
# Ensure the request context is passed through!
kwargs['context'] = self.get_serializer_context()
@@ -1141,6 +1147,18 @@ class BomList(generics.ListCreateAPIView):
except (ValueError, Part.DoesNotExist):
pass
include_pricing = str2bool(params.get('include_pricing', True))
if include_pricing:
queryset = self.annotate_pricing(queryset)
return queryset
def annotate_pricing(self, queryset):
"""
Add part pricing information to the queryset
"""
# Annotate with purchase prices
queryset = queryset.annotate(
purchase_price_min=Min('sub_part__stock_items__purchase_price'),

View File

@@ -1,13 +1,9 @@
from __future__ import unicode_literals
import os
import logging
from django.db.utils import OperationalError, ProgrammingError
from django.apps import AppConfig
from django.conf import settings
from PIL import UnidentifiedImageError
from InvenTree.ready import canAppAccessDatabase
@@ -24,40 +20,8 @@ class PartConfig(AppConfig):
"""
if canAppAccessDatabase():
self.generate_part_thumbnails()
self.update_trackable_status()
def generate_part_thumbnails(self):
"""
Generate thumbnail images for any Part that does not have one.
This function exists mainly for legacy support,
as any *new* image uploaded will have a thumbnail generated automatically.
"""
from .models import Part
logger.debug("InvenTree: Checking Part image thumbnails")
try:
# Only check parts which have images
for part in Part.objects.exclude(image=None):
if part.image:
url = part.image.thumbnail.name
loc = os.path.join(settings.MEDIA_ROOT, url)
if not os.path.exists(loc):
logger.info("InvenTree: Generating thumbnail for Part '{p}'".format(p=part.name))
try:
part.image.render_variations(replace=False)
except FileNotFoundError:
logger.warning(f"Image file '{part.image}' missing")
pass
except UnidentifiedImageError:
logger.warning(f"Image file '{part.image}' is invalid")
except (OperationalError, ProgrammingError):
# Exception if the database has not been migrated yet
pass
def update_trackable_status(self):
"""
Check for any instances where a trackable part is used in the BOM
@@ -72,7 +36,7 @@ class PartConfig(AppConfig):
items = BomItem.objects.filter(part__trackable=False, sub_part__trackable=True)
for item in items:
print(f"Marking part '{item.part.name}' as trackable")
logger.info(f"Marking part '{item.part.name}' as trackable")
item.part.trackable = True
item.part.clean()
item.part.save()

View File

@@ -189,12 +189,15 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Process manufacturer part
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts):
if manufacturer_part:
if manufacturer_part and manufacturer_part.manufacturer:
manufacturer_name = manufacturer_part.manufacturer.name
else:
manufacturer_name = ''
manufacturer_mpn = manufacturer_part.MPN
if manufacturer_part:
manufacturer_mpn = manufacturer_part.MPN
else:
manufacturer_mpn = ''
# Generate column names for this manufacturer
k_man = manufacturer_headers[0] + "_" + str(manufacturer_idx)
@@ -210,12 +213,15 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Process supplier parts
for supplier_idx, supplier_part in enumerate(manufacturer_part.supplier_parts.all()):
if supplier_part.supplier:
if supplier_part.supplier and supplier_part.supplier:
supplier_name = supplier_part.supplier.name
else:
supplier_name = ''
supplier_sku = supplier_part.SKU
if supplier_part:
supplier_sku = supplier_part.SKU
else:
supplier_sku = ''
# Generate column names for this supplier
k_sup = str(supplier_headers[0]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)

View File

@@ -30,4 +30,11 @@
fields:
part: 100
sub_part: 50
quantity: 3
quantity: 3
- model: part.bomitem
pk: 5
fields:
part: 1
sub_part: 5
quantity: 3

View File

@@ -4,6 +4,7 @@ Part database model definitions
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import decimal
import os
import logging
@@ -1530,10 +1531,13 @@ class Part(MPTTModel):
for item in self.get_bom_items().all().select_related('sub_part'):
if item.sub_part.pk == self.pk:
print("Warning: Item contains itself in BOM")
logger.warning(f"WARNING: BomItem ID {item.pk} contains itself in BOM")
continue
prices = item.sub_part.get_price_range(quantity * item.quantity, internal=internal, purchase=purchase)
q = decimal.Decimal(quantity)
i = decimal.Decimal(item.quantity)
prices = item.sub_part.get_price_range(q * i, internal=internal, purchase=purchase)
if prices is None:
continue
@@ -2329,6 +2333,23 @@ class BomItem(models.Model):
def get_api_url():
return reverse('api-bom-list')
def get_stock_filter(self):
"""
Return a queryset filter for selecting StockItems which match this BomItem
- If allow_variants is True, allow all part variants
"""
# Target part
part = self.sub_part
if self.allow_variants:
variants = part.get_descendants(include_self=True)
return Q(part__in=[v.pk for v in variants])
else:
return Q(part=part)
def save(self, *args, **kwargs):
self.clean()

View File

@@ -193,6 +193,7 @@ class PartBriefSerializer(InvenTreeModelSerializer):
fields = [
'pk',
'IPN',
'default_location',
'name',
'revision',
'full_name',
@@ -418,6 +419,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
part_detail = kwargs.pop('part_detail', False)
sub_part_detail = kwargs.pop('sub_part_detail', False)
include_pricing = kwargs.pop('include_pricing', False)
super(BomItemSerializer, self).__init__(*args, **kwargs)
@@ -427,6 +429,14 @@ class BomItemSerializer(InvenTreeModelSerializer):
if sub_part_detail is not True:
self.fields.pop('sub_part_detail')
if not include_pricing:
# Remove all pricing related fields
self.fields.pop('price_range')
self.fields.pop('purchase_price_min')
self.fields.pop('purchase_price_max')
self.fields.pop('purchase_price_avg')
self.fields.pop('purchase_price_range')
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related('part')

View File

@@ -328,6 +328,12 @@
// If image / thumbnail data present, live update
if (data.image) {
$('#part-image').attr('src', data.image);
// Reset the "modal image" view
$('#part-thumb').click(function() {
showModalImage(data.image);
});
} else {
// Otherwise, reload the page
location.reload();
@@ -372,7 +378,7 @@
{
success: function(items) {
adjustStock(action, items, {
onSuccess: function() {
success: function() {
location.reload();
}
});

View File

@@ -277,7 +277,7 @@ class PartAPITest(InvenTreeAPITestCase):
""" There should be 4 BomItem objects in the database """
url = reverse('api-bom-list')
response = self.client.get(url, format='json')
self.assertEqual(len(response.data), 4)
self.assertEqual(len(response.data), 5)
def test_get_bom_detail(self):
# Get the detail for a single BomItem

View File

@@ -120,7 +120,13 @@ class BomItemTest(TestCase):
def test_pricing(self):
self.bob.get_price(1)
self.assertEqual(self.bob.get_bom_price_range(1, internal=True), (Decimal(84.5), Decimal(89.5)))
self.assertEqual(
self.bob.get_bom_price_range(1, internal=True),
(Decimal(29.5), Decimal(89.5))
)
# remove internal price for R_2K2_0805
self.r1.internal_price_breaks.delete()
self.assertEqual(self.bob.get_bom_price_range(1, internal=True), (Decimal(82.5), Decimal(87.5)))
self.assertEqual(
self.bob.get_bom_price_range(1, internal=True),
(Decimal(27.5), Decimal(87.5))
)