Merge branch 'master' of https://github.com/inventree/InvenTree into price-history

This commit is contained in:
Matthias 2021-04-17 09:59:05 +02:00
commit 3598c36043
60 changed files with 2580 additions and 218 deletions

View File

@ -280,11 +280,25 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs):
json string of the supplied data plus some other data json string of the supplied data plus some other data
""" """
url = kwargs.get('url', False)
brief = kwargs.get('brief', True) brief = kwargs.get('brief', True)
data = {} data = {}
if brief: if url:
request = object_data.get('request', None)
item_url = object_data.get('item_url', None)
absolute_url = None
if request and item_url:
absolute_url = request.build_absolute_uri(item_url)
# Return URL (No JSON)
return absolute_url
if item_url:
# Return URL (No JSON)
return item_url
elif brief:
data[object_name] = object_pk data[object_name] = object_pk
else: else:
data['tool'] = 'InvenTree' data['tool'] = 'InvenTree'

View File

@ -185,6 +185,10 @@
color: #c55; color: #c55;
} }
.icon-orange {
color: #fcba03;
}
.icon-green { .icon-green {
color: #43bb43; color: #43bb43;
} }

View File

@ -11,6 +11,7 @@ from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from company.urls import company_urls from company.urls import company_urls
from company.urls import manufacturer_part_urls
from company.urls import supplier_part_urls from company.urls import supplier_part_urls
from company.urls import price_break_urls from company.urls import price_break_urls
@ -115,6 +116,7 @@ dynamic_javascript_urls = [
urlpatterns = [ urlpatterns = [
url(r'^part/', include(part_urls)), url(r'^part/', include(part_urls)),
url(r'^manufacturer-part/', include(manufacturer_part_urls)),
url(r'^supplier-part/', include(supplier_part_urls)), url(r'^supplier-part/', include(supplier_part_urls)),
url(r'^price-break/', include(price_break_urls)), url(r'^price-break/', include(price_break_urls)),

View File

@ -15,9 +15,11 @@ from django.db.models import Q
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
from .models import Company from .models import Company
from .models import ManufacturerPart
from .models import SupplierPart, SupplierPriceBreak from .models import SupplierPart, SupplierPriceBreak
from .serializers import CompanySerializer from .serializers import CompanySerializer
from .serializers import ManufacturerPartSerializer
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
@ -80,8 +82,105 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = CompanySerializer.annotate_queryset(queryset) queryset = CompanySerializer.annotate_queryset(queryset)
return queryset return queryset
class ManufacturerPartList(generics.ListCreateAPIView):
""" API endpoint for list view of ManufacturerPart object
- GET: Return list of ManufacturerPart objects
- POST: Create a new ManufacturerPart object
"""
queryset = ManufacturerPart.objects.all().prefetch_related(
'part',
'manufacturer',
'supplier_parts',
)
serializer_class = ManufacturerPartSerializer
def get_serializer(self, *args, **kwargs):
# Do we wish to include extra detail?
try:
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
except AttributeError:
pass
try:
kwargs['manufacturer_detail'] = str2bool(self.request.query_params.get('manufacturer_detail', None))
except AttributeError:
pass
try:
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def filter_queryset(self, queryset):
"""
Custom filtering for the queryset.
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filter by manufacturer
manufacturer = params.get('company', None)
if manufacturer is not None:
queryset = queryset.filter(manufacturer=manufacturer)
# Filter by parent part?
part = params.get('part', None)
if part is not None:
queryset = queryset.filter(part=part)
# Filter by 'active' status of the part?
active = params.get('active', None)
if active is not None:
active = str2bool(active)
queryset = queryset.filter(part__active=active)
return queryset
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
]
search_fields = [
'manufacturer__name',
'description',
'MPN',
'part__name',
'part__description',
]
class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of ManufacturerPart object
- GET: Retrieve detail view
- PATCH: Update object
- DELETE: Delete object
"""
queryset = ManufacturerPart.objects.all()
serializer_class = ManufacturerPartSerializer
class SupplierPartList(generics.ListCreateAPIView): class SupplierPartList(generics.ListCreateAPIView):
""" API endpoint for list view of SupplierPart object """ API endpoint for list view of SupplierPart object
@ -92,7 +191,7 @@ class SupplierPartList(generics.ListCreateAPIView):
queryset = SupplierPart.objects.all().prefetch_related( queryset = SupplierPart.objects.all().prefetch_related(
'part', 'part',
'supplier', 'supplier',
'manufacturer' 'manufacturer_part__manufacturer',
) )
def get_queryset(self): def get_queryset(self):
@ -114,7 +213,7 @@ class SupplierPartList(generics.ListCreateAPIView):
manufacturer = params.get('manufacturer', None) manufacturer = params.get('manufacturer', None)
if manufacturer is not None: if manufacturer is not None:
queryset = queryset.filter(manufacturer=manufacturer) queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer)
# Filter by supplier # Filter by supplier
supplier = params.get('supplier', None) supplier = params.get('supplier', None)
@ -126,7 +225,7 @@ class SupplierPartList(generics.ListCreateAPIView):
company = params.get('company', None) company = params.get('company', None)
if company is not None: if company is not None:
queryset = queryset.filter(Q(manufacturer=company) | Q(supplier=company)) queryset = queryset.filter(Q(manufacturer_part__manufacturer=company) | Q(supplier=company))
# Filter by parent part? # Filter by parent part?
part = params.get('part', None) part = params.get('part', None)
@ -134,6 +233,12 @@ class SupplierPartList(generics.ListCreateAPIView):
if part is not None: if part is not None:
queryset = queryset.filter(part=part) queryset = queryset.filter(part=part)
# Filter by manufacturer part?
manufacturer_part = params.get('manufacturer_part', None)
if manufacturer_part is not None:
queryset = queryset.filter(manufacturer_part=manufacturer_part)
# Filter by 'active' status of the part? # Filter by 'active' status of the part?
active = params.get('active', None) active = params.get('active', None)
@ -184,9 +289,9 @@ class SupplierPartList(generics.ListCreateAPIView):
search_fields = [ search_fields = [
'SKU', 'SKU',
'supplier__name', 'supplier__name',
'manufacturer__name', 'manufacturer_part__manufacturer__name',
'description', 'description',
'MPN', 'manufacturer_part__MPN',
'part__name', 'part__name',
'part__description', 'part__description',
] ]
@ -197,7 +302,7 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
- GET: Retrieve detail view - GET: Retrieve detail view
- PATCH: Update object - PATCH: Update object
- DELETE: Delete objec - DELETE: Delete object
""" """
queryset = SupplierPart.objects.all() queryset = SupplierPart.objects.all()
@ -226,6 +331,15 @@ class SupplierPriceBreakList(generics.ListCreateAPIView):
] ]
manufacturer_part_api_urls = [
url(r'^(?P<pk>\d+)/?', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'),
# Catch anything else
url(r'^.*$', ManufacturerPartList.as_view(), name='api-manufacturer-part-list'),
]
supplier_part_api_urls = [ supplier_part_api_urls = [
url(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'), url(r'^(?P<pk>\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
@ -236,7 +350,8 @@ supplier_part_api_urls = [
company_api_urls = [ company_api_urls = [
url(r'^part/manufacturer/', include(manufacturer_part_api_urls)),
url(r'^part/', include(supplier_part_api_urls)), url(r'^part/', include(supplier_part_api_urls)),
url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'), url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'),

View File

@ -31,3 +31,17 @@
name: Another customer! name: Another customer!
description: Yet another company description: Yet another company
is_customer: True is_customer: True
- model: company.company
pk: 6
fields:
name: A manufacturer
description: A company that makes parts!
is_manufacturer: True
- model: company.company
pk: 7
fields:
name: Another manufacturer
description: They build things and sell it to us
is_manufacturer: True

View File

@ -0,0 +1,39 @@
# Manufacturer Parts
- model: company.manufacturerpart
pk: 1
fields:
part: 5
manufacturer: 6
MPN: 'MPN123'
- model: company.manufacturerpart
pk: 2
fields:
part: 3
manufacturer: 7
MPN: 'MPN456'
- model: company.manufacturerpart
pk: 3
fields:
part: 5
manufacturer: 7
MPN: 'MPN789'
# Supplier parts linked to Manufacturer parts
- model: company.supplierpart
pk: 10
fields:
part: 3
manufacturer_part: 2
supplier: 2
SKU: 'MPN456-APPEL'
- model: company.supplierpart
pk: 11
fields:
part: 3
manufacturer_part: 2
supplier: 3
SKU: 'MPN456-ZERG'

View File

@ -17,6 +17,7 @@ from djmoney.forms.fields import MoneyField
import common.settings import common.settings
from .models import Company from .models import Company
from .models import ManufacturerPart
from .models import SupplierPart from .models import SupplierPart
from .models import SupplierPriceBreak from .models import SupplierPriceBreak
@ -85,12 +86,30 @@ class CompanyImageDownloadForm(HelperForm):
] ]
class EditManufacturerPartForm(HelperForm):
""" Form for editing a ManufacturerPart object """
field_prefix = {
'link': 'fa-link',
'MPN': 'fa-hashtag',
}
class Meta:
model = ManufacturerPart
fields = [
'part',
'manufacturer',
'MPN',
'description',
'link',
]
class EditSupplierPartForm(HelperForm): class EditSupplierPartForm(HelperForm):
""" Form for editing a SupplierPart object """ """ Form for editing a SupplierPart object """
field_prefix = { field_prefix = {
'link': 'fa-link', 'link': 'fa-link',
'MPN': 'fa-hashtag',
'SKU': 'fa-hashtag', 'SKU': 'fa-hashtag',
'note': 'fa-pencil-alt', 'note': 'fa-pencil-alt',
} }
@ -104,15 +123,28 @@ class EditSupplierPartForm(HelperForm):
required=False, required=False,
) )
manufacturer = django.forms.ChoiceField(
required=False,
help_text=_('Select manufacturer'),
choices=[],
)
MPN = django.forms.CharField(
required=False,
help_text=_('Manufacturer Part Number'),
max_length=100,
label=_('MPN'),
)
class Meta: class Meta:
model = SupplierPart model = SupplierPart
fields = [ fields = [
'part', 'part',
'supplier', 'supplier',
'SKU', 'SKU',
'description',
'manufacturer', 'manufacturer',
'MPN', 'MPN',
'description',
'link', 'link',
'note', 'note',
'single_pricing', 'single_pricing',
@ -121,6 +153,19 @@ class EditSupplierPartForm(HelperForm):
'packaging', 'packaging',
] ]
def get_manufacturer_choices(self):
""" Returns tuples for all manufacturers """
empty_choice = [('', '----------')]
manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)]
return empty_choice + manufacturers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['manufacturer'].choices = self.get_manufacturer_choices()
class EditPriceBreakForm(HelperForm): class EditPriceBreakForm(HelperForm):
""" Form for creating / editing a supplier price break """ """ Form for creating / editing a supplier price break """

View File

@ -0,0 +1,27 @@
import InvenTree.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('company', '0033_auto_20210410_1528'),
]
operations = [
migrations.CreateModel(
name='ManufacturerPart',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('MPN', models.CharField(help_text='Manufacturer Part Number', max_length=100, null=True, verbose_name='MPN')),
('link', InvenTree.fields.InvenTreeURLField(blank=True, help_text='URL for external manufacturer part link', null=True, verbose_name='Link')),
('description', models.CharField(blank=True, help_text='Manufacturer part description', max_length=250, null=True, verbose_name='Description')),
('manufacturer', models.ForeignKey(help_text='Select manufacturer', limit_choices_to={'is_manufacturer': True}, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='manufactured_parts', to='company.Company', verbose_name='Manufacturer')),
('part', models.ForeignKey(help_text='Select part', limit_choices_to={'purchaseable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='manufacturer_parts', to='part.Part', verbose_name='Base Part')),
],
options={
'unique_together': {('part', 'manufacturer', 'MPN')},
},
),
]

View File

@ -0,0 +1,18 @@
import InvenTree.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('company', '0034_manufacturerpart'),
]
operations = [
migrations.AddField(
model_name='supplierpart',
name='manufacturer_part',
field=models.ForeignKey(blank=True, help_text='Select manufacturer part', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='supplier_parts', to='company.ManufacturerPart', verbose_name='Manufacturer Part'),
),
]

View File

@ -0,0 +1,110 @@
import InvenTree.fields
from django.db import migrations, models, transaction
import django.db.models.deletion
from django.db.utils import IntegrityError
def supplierpart_make_manufacturer_parts(apps, schema_editor):
Part = apps.get_model('part', 'Part')
ManufacturerPart = apps.get_model('company', 'ManufacturerPart')
SupplierPart = apps.get_model('company', 'SupplierPart')
supplier_parts = SupplierPart.objects.all()
if supplier_parts:
print(f'\nCreating ManufacturerPart Objects\n{"-"*10}')
for supplier_part in supplier_parts:
print(f'{supplier_part.supplier.name[:15].ljust(15)} | {supplier_part.SKU[:15].ljust(15)}\t', end='')
if supplier_part.manufacturer_part:
print(f'[ERROR: MANUFACTURER PART ALREADY EXISTS]')
continue
part = supplier_part.part
if not part:
print(f'[ERROR: SUPPLIER PART IS NOT CONNECTED TO PART]')
continue
manufacturer = supplier_part.manufacturer
MPN = supplier_part.MPN
link = supplier_part.link
description = supplier_part.description
if manufacturer or MPN:
print(f' | {part.name[:15].ljust(15)}', end='')
try:
print(f' | {manufacturer.name[:15].ljust(15)}', end='')
except AttributeError:
print(f' | {"EMPTY MANUF".ljust(15)}', end='')
try:
print(f' | {MPN[:15].ljust(15)}', end='')
except TypeError:
print(f' | {"EMPTY MPN".ljust(15)}', end='')
print('\t', end='')
# Create ManufacturerPart
manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=MPN, description=description, link=link)
created = False
try:
with transaction.atomic():
manufacturer_part.save()
created = True
except IntegrityError:
manufacturer_part = ManufacturerPart.objects.get(part=part, manufacturer=manufacturer, MPN=MPN)
# Link it to SupplierPart
supplier_part.manufacturer_part = manufacturer_part
supplier_part.save()
if created:
print(f'[SUCCESS: MANUFACTURER PART CREATED]')
else:
print(f'[IGNORED: MANUFACTURER PART ALREADY EXISTS]')
else:
print(f'[IGNORED: MISSING MANUFACTURER DATA]')
print(f'{"-"*10}\nDone\n')
def supplierpart_populate_manufacturer_info(apps, schema_editor):
Part = apps.get_model('part', 'Part')
ManufacturerPart = apps.get_model('company', 'ManufacturerPart')
SupplierPart = apps.get_model('company', 'SupplierPart')
supplier_parts = SupplierPart.objects.all()
if supplier_parts:
print(f'\nSupplierPart: Populating Manufacturer Information\n{"-"*10}')
for supplier_part in supplier_parts:
print(f'{supplier_part.supplier.name[:15].ljust(15)} | {supplier_part.SKU[:15].ljust(15)}\t', end='')
manufacturer_part = supplier_part.manufacturer_part
if manufacturer_part:
if manufacturer_part.manufacturer:
supplier_part.manufacturer = manufacturer_part.manufacturer
if manufacturer_part.MPN:
supplier_part.MPN = manufacturer_part.MPN
supplier_part.save()
print(f'[SUCCESS: UPDATED MANUFACTURER INFO]')
else:
print(f'[IGNORED: NO MANUFACTURER PART]')
print(f'{"-"*10}\nDone\n')
class Migration(migrations.Migration):
dependencies = [
('company', '0035_supplierpart_update_1'),
]
operations = [
# Make new ManufacturerPart with SupplierPart "manufacturer" and "MPN"
# fields, then link it to the new SupplierPart "manufacturer_part" field
migrations.RunPython(supplierpart_make_manufacturer_parts, reverse_code=supplierpart_populate_manufacturer_info),
]

View File

@ -0,0 +1,21 @@
import InvenTree.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('company', '0036_supplierpart_update_2'),
]
operations = [
migrations.RemoveField(
model_name='supplierpart',
name='MPN',
),
migrations.RemoveField(
model_name='supplierpart',
name='manufacturer',
),
]

View File

@ -11,7 +11,9 @@ import math
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.utils import IntegrityError
from django.db.models import Sum, Q, UniqueConstraint from django.db.models import Sum, Q, UniqueConstraint
from django.apps import apps from django.apps import apps
@ -208,7 +210,7 @@ class Company(models.Model):
@property @property
def parts(self): def parts(self):
""" Return SupplierPart objects which are supplied or manufactured by this company """ """ Return SupplierPart objects which are supplied or manufactured by this company """
return SupplierPart.objects.filter(Q(supplier=self.id) | Q(manufacturer=self.id)) return SupplierPart.objects.filter(Q(supplier=self.id) | Q(manufacturer_part__manufacturer=self.id))
@property @property
def part_count(self): def part_count(self):
@ -223,7 +225,7 @@ class Company(models.Model):
def stock_items(self): def stock_items(self):
""" Return a list of all stock items supplied or manufactured by this company """ """ Return a list of all stock items supplied or manufactured by this company """
stock = apps.get_model('stock', 'StockItem') stock = apps.get_model('stock', 'StockItem')
return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer=self.id)).all() return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer_part__manufacturer=self.id)).all()
@property @property
def stock_count(self): def stock_count(self):
@ -284,19 +286,106 @@ class Contact(models.Model):
on_delete=models.CASCADE) on_delete=models.CASCADE)
class SupplierPart(models.Model): class ManufacturerPart(models.Model):
""" Represents a unique part as provided by a Supplier """ Represents a unique part as provided by a Manufacturer
Each SupplierPart is identified by a MPN (Manufacturer Part Number) Each ManufacturerPart is identified by a MPN (Manufacturer Part Number)
Each SupplierPart is also linked to a Part object. Each ManufacturerPart is also linked to a Part object.
A Part may be available from multiple suppliers A Part may be available from multiple manufacturers
Attributes: Attributes:
part: Link to the master Part part: Link to the master Part
manufacturer: Company that manufactures the ManufacturerPart
MPN: Manufacture part number
link: Link to external website for this manufacturer part
description: Descriptive notes field
"""
class Meta:
unique_together = ('part', 'manufacturer', 'MPN')
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='manufacturer_parts',
verbose_name=_('Base Part'),
limit_choices_to={
'purchaseable': True,
},
help_text=_('Select part'),
)
manufacturer = models.ForeignKey(
Company,
on_delete=models.CASCADE,
null=True,
related_name='manufactured_parts',
limit_choices_to={
'is_manufacturer': True
},
verbose_name=_('Manufacturer'),
help_text=_('Select manufacturer'),
)
MPN = models.CharField(
null=True,
max_length=100,
verbose_name=_('MPN'),
help_text=_('Manufacturer Part Number')
)
link = InvenTreeURLField(
blank=True, null=True,
verbose_name=_('Link'),
help_text=_('URL for external manufacturer part link')
)
description = models.CharField(
max_length=250, blank=True, null=True,
verbose_name=_('Description'),
help_text=_('Manufacturer part description')
)
@classmethod
def create(cls, part, manufacturer, mpn, description, link=None):
""" Check if ManufacturerPart instance does not already exist
then create it
"""
manufacturer_part = None
try:
manufacturer_part = ManufacturerPart.objects.get(part=part, manufacturer=manufacturer, MPN=mpn)
except ManufacturerPart.DoesNotExist:
pass
if not manufacturer_part:
manufacturer_part = ManufacturerPart(part=part, manufacturer=manufacturer, MPN=mpn, description=description, link=link)
manufacturer_part.save()
return manufacturer_part
def __str__(self):
s = ''
if self.manufacturer:
s += f'{self.manufacturer.name}'
s += ' | '
s += f'{self.MPN}'
return s
class SupplierPart(models.Model):
""" Represents a unique part as provided by a Supplier
Each SupplierPart is identified by a SKU (Supplier Part Number)
Each SupplierPart is also linked to a Part or ManufacturerPart object.
A Part may be available from multiple suppliers
Attributes:
part: Link to the master Part (Obsolete)
source_item: The sourcing item linked to this SupplierPart instance
supplier: Company that supplies this SupplierPart object supplier: Company that supplies this SupplierPart object
SKU: Stock keeping unit (supplier part number) SKU: Stock keeping unit (supplier part number)
manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!) link: Link to external website for this supplier part
MPN: Manufacture part number
link: Link to external website for this part
description: Descriptive notes field description: Descriptive notes field
note: Longer form note field note: Longer form note field
base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee" base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee"
@ -308,6 +397,57 @@ class SupplierPart(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('supplier-part-detail', kwargs={'pk': self.id}) return reverse('supplier-part-detail', kwargs={'pk': self.id})
def save(self, *args, **kwargs):
""" Overriding save method to process the linked ManufacturerPart
"""
if 'manufacturer' in kwargs:
manufacturer_id = kwargs.pop('manufacturer')
try:
manufacturer = Company.objects.get(pk=int(manufacturer_id))
except (ValueError, Company.DoesNotExist):
manufacturer = None
else:
manufacturer = None
if 'MPN' in kwargs:
MPN = kwargs.pop('MPN')
else:
MPN = None
if manufacturer or MPN:
if not self.manufacturer_part:
# Create ManufacturerPart
manufacturer_part = ManufacturerPart.create(part=self.part,
manufacturer=manufacturer,
mpn=MPN,
description=self.description)
self.manufacturer_part = manufacturer_part
else:
# Update ManufacturerPart (if ID exists)
try:
manufacturer_part_id = self.manufacturer_part.id
except AttributeError:
manufacturer_part_id = None
if manufacturer_part_id:
try:
(manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part,
manufacturer=manufacturer,
MPN=MPN)
except IntegrityError:
manufacturer_part = None
raise ValidationError(f'ManufacturerPart linked to {self.part} from manufacturer {manufacturer.name}'
f'with part number {MPN} already exists!')
if manufacturer_part:
self.manufacturer_part = manufacturer_part
self.clean()
self.validate_unique()
super().save(*args, **kwargs)
class Meta: class Meta:
unique_together = ('part', 'supplier', 'SKU') unique_together = ('part', 'supplier', 'SKU')
@ -336,23 +476,12 @@ class SupplierPart(models.Model):
help_text=_('Supplier stock keeping unit') help_text=_('Supplier stock keeping unit')
) )
manufacturer = models.ForeignKey( manufacturer_part = models.ForeignKey(ManufacturerPart, on_delete=models.CASCADE,
Company, blank=True, null=True,
on_delete=models.SET_NULL, related_name='supplier_parts',
related_name='manufactured_parts', verbose_name=_('Manufacturer Part'),
limit_choices_to={ help_text=_('Select manufacturer part'),
'is_manufacturer': True )
},
verbose_name=_('Manufacturer'),
help_text=_('Select manufacturer'),
null=True, blank=True
)
MPN = models.CharField(
max_length=100, blank=True, null=True,
verbose_name=_('MPN'),
help_text=_('Manufacturer part number')
)
link = InvenTreeURLField( link = InvenTreeURLField(
blank=True, null=True, blank=True, null=True,
@ -389,10 +518,11 @@ class SupplierPart(models.Model):
items = [] items = []
if self.manufacturer: if self.manufacturer_part:
items.append(self.manufacturer.name) if self.manufacturer_part.manufacturer:
if self.MPN: items.append(self.manufacturer_part.manufacturer.name)
items.append(self.MPN) if self.manufacturer_part.MPN:
items.append(self.manufacturer_part.MPN)
return ' | '.join(items) return ' | '.join(items)

View File

@ -7,6 +7,7 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount from sql_util.utils import SubqueryCount
from .models import Company from .models import Company
from .models import ManufacturerPart
from .models import SupplierPart, SupplierPriceBreak from .models import SupplierPart, SupplierPriceBreak
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
@ -80,6 +81,49 @@ class CompanySerializer(InvenTreeModelSerializer):
] ]
class ManufacturerPartSerializer(InvenTreeModelSerializer):
""" Serializer for ManufacturerPart object """
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True)
pretty_name = serializers.CharField(read_only=True)
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
manufacturer_detail = kwargs.pop('manufacturer_detail', False)
prettify = kwargs.pop('pretty', False)
super(ManufacturerPartSerializer, self).__init__(*args, **kwargs)
if part_detail is not True:
self.fields.pop('part_detail')
if manufacturer_detail is not True:
self.fields.pop('manufacturer_detail')
if prettify is not True:
self.fields.pop('pretty_name')
manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True))
class Meta:
model = ManufacturerPart
fields = [
'pk',
'part',
'part_detail',
'pretty_name',
'manufacturer',
'manufacturer_detail',
'description',
'MPN',
'link',
]
class SupplierPartSerializer(InvenTreeModelSerializer): class SupplierPartSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPart object """ """ Serializer for SupplierPart object """
@ -87,7 +131,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True) supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True) manufacturer_detail = CompanyBriefSerializer(source='manufacturer_part.manufacturer', many=False, read_only=True)
pretty_name = serializers.CharField(read_only=True) pretty_name = serializers.CharField(read_only=True)
@ -113,8 +157,12 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
self.fields.pop('pretty_name') self.fields.pop('pretty_name')
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True)) supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True)
MPN = serializers.StringRelatedField(source='manufacturer_part.MPN')
manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True)) manufacturer_part = ManufacturerPartSerializer(read_only=True)
class Meta: class Meta:
model = SupplierPart model = SupplierPart
@ -127,12 +175,31 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
'supplier_detail', 'supplier_detail',
'SKU', 'SKU',
'manufacturer', 'manufacturer',
'manufacturer_detail',
'description',
'MPN', 'MPN',
'manufacturer_detail',
'manufacturer_part',
'description',
'link', 'link',
] ]
def create(self, validated_data):
""" Extract manufacturer data and process ManufacturerPart """
# Create SupplierPart
supplier_part = super().create(validated_data)
# Get ManufacturerPart raw data (unvalidated)
manufacturer_id = self.initial_data.get('manufacturer', None)
MPN = self.initial_data.get('MPN', None)
if manufacturer_id or MPN:
kwargs = {'manufacturer': manufacturer_id,
'MPN': MPN,
}
supplier_part.save(**kwargs)
return supplier_part
class SupplierPriceBreakSerializer(InvenTreeModelSerializer): class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPriceBreak object """ """ Serializer for SupplierPriceBreak object """

View File

@ -21,11 +21,13 @@
<td>{% trans "Company Name" %}</td> <td>{% trans "Company Name" %}</td>
<td>{{ company.name }}</td> <td>{{ company.name }}</td>
</tr> </tr>
{% if company.description %}
<tr> <tr>
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Description" %}</td> <td>{% trans "Description" %}</td>
<td>{{ company.description }}</td> <td>{{ company.description }}</td>
</tr> </tr>
{% endif %}
<tr> <tr>
<td><span class='fas fa-globe'></span></td> <td><span class='fas fa-globe'></span></td>
<td>{% trans "Website" %}</td> <td>{% trans "Website" %}</td>

View File

@ -0,0 +1,127 @@
{% extends "company/company_base.html" %}
{% load static %}
{% load i18n %}
{% load inventree_extras %}
{% block menubar %}
{% include 'company/navbar.html' with tab='manufacturer_parts' %}
{% endblock %}
{% block heading %}
{% trans "Manufacturer Parts" %}
{% endblock %}
{% block details %}
{% if roles.purchase_order.change %}
<div id='button-toolbar'>
<div class='button-toolbar container-fluid'>
<div class='btn-group role='group'>
{% if roles.purchase_order.add %}
<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" %}
</button>
{% endif %}
<div class='btn-group'>
<div class="dropdown" style="float: right;">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if roles.purchase_order.add %}
<li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
{% endif %}
{% if roles.purchase_order.delete %}
<li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class='filter-list' id='filter-list-supplier-part'>
<!-- Empty div (will be filled out with available BOM filters) -->
</div>
</div>
</div>
{% endif %}
<table class='table table-striped table-condensed' id='part-table' data-toolbar='#button-toolbar'>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$("#manufacturer-part-create").click(function () {
launchModalForm(
"{% url 'manufacturer-part-create' %}",
{
data: {
manufacturer: {{ company.id }},
},
reload: true,
secondary: [
{
field: 'part',
label: '{% trans "New Part" %}',
title: '{% trans "Create new Part" %}',
url: "{% url 'part-create' %}"
},
{
field: 'manufacturer',
label: '{% trans "New Manufacturer" %}',
title: '{% trans "Create new Manufacturer" %}',
url: "{% url 'manufacturer-create' %}",
},
]
});
});
loadManufacturerPartTable(
"#part-table",
"{% url 'api-manufacturer-part-list' %}",
{
params: {
part_detail: true,
manufacturer_detail: true,
company: {{ company.id }},
},
}
);
$("#multi-part-delete").click(function() {
var selections = $("#part-table").bootstrapTable("getSelections");
var parts = [];
selections.forEach(function(item) {
parts.push(item.pk);
});
var url = "{% url 'manufacturer-part-delete' %}"
launchModalForm(url, {
data: {
parts: parts,
},
reload: true,
});
});
$("#multi-part-order").click(function() {
var selections = $("#part-table").bootstrapTable("getSelections");
var parts = [];
selections.forEach(function(item) {
parts.push(item.part);
});
launchModalForm("/order/purchase-order/order-parts/", {
data: {
parts: parts,
},
});
});
{% endblock %}

View File

@ -1,9 +1,10 @@
{% extends "company/company_base.html" %} {% extends "company/company_base.html" %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load inventree_extras %}
{% block menubar %} {% block menubar %}
{% include 'company/navbar.html' with tab='parts' %} {% include 'company/navbar.html' with tab='supplier_parts' %}
{% endblock %} {% endblock %}
{% block heading %} {% block heading %}
@ -17,9 +18,9 @@
<div class='button-toolbar container-fluid'> <div class='button-toolbar container-fluid'>
<div class='btn-group' role='group'> <div class='btn-group' role='group'>
{% if roles.purchase_order.add %} {% if roles.purchase_order.add %}
<button class="btn btn-success" id='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>
{% endif %} {% endif %}
<div class='btn-group'> <div class='btn-group'>
<div class="dropdown" style="float: right;"> <div class="dropdown" style="float: right;">
@ -51,13 +52,12 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
$("#part-create").click(function () { $("#supplier-part-create").click(function () {
launchModalForm( launchModalForm(
"{% url 'supplier-part-create' %}", "{% url 'supplier-part-create' %}",
{ {
data: { data: {
{% if company.is_supplier %}supplier: {{ company.id }},{% endif %} supplier: {{ company.id }},
{% if company.is_manufacturer %}manufacturer: {{ company.id }},{% endif %}
}, },
reload: true, reload: true,
secondary: [ secondary: [
@ -73,12 +73,6 @@
title: "{% trans 'Create new Supplier' %}", title: "{% trans 'Create new Supplier' %}",
url: "{% url 'supplier-create' %}", url: "{% url 'supplier-create' %}",
}, },
{
field: 'manufacturer',
label: '{% trans "New Manufacturer" %}',
title: '{% trans "Create new Manufacturer" %}',
url: "{% url 'manufacturer-create' %}",
},
] ]
}); });
}); });
@ -105,7 +99,9 @@
parts.push(item.pk); parts.push(item.pk);
}); });
launchModalForm("{% url 'supplier-part-delete' %}", { var url = "{% url 'supplier-part-delete' %}"
launchModalForm(url, {
data: { data: {
parts: parts, parts: parts,
}, },

View File

@ -0,0 +1,133 @@
{% extends "two_column.html" %}
{% load static %}
{% load i18n %}
{% block page_title %}
InvenTree | {% trans "Manufacturer Part" %}
{% endblock %}
{% block thumbnail %}
<img class='part-thumb'
{% if part.part.image %}
src='{{ part.part.image.url }}'
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
{% endblock %}
{% block page_data %}
<h3>{% trans "Manufacturer Part" %}</h3>
<hr>
<h4>
{{ part.part.full_name }}
{% if user.is_staff and perms.company.change_company %}
<a href="{% url 'admin:company_supplierpart_change' part.pk %}">
<span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span>
</a>
{% endif %}
</h4>
<p>{{ part.manufacturer.name }} - {{ part.MPN }}</p>
{% if roles.purchase_order.change %}
<div class='btn-row'>
<div class='btn-group action-buttons' role='group'>
{% comment "for later" %}
{% if roles.purchase_order.add %}
<button type='button' class='btn btn-default btn-glyph' id='order-part' title='{% trans "Order part" %}'>
<span class='fas fa-shopping-cart'></span>
</button>
{% endif %}
{% endcomment %}
<button type='button' class='btn btn-default btn-glyph' id='edit-part' title='{% trans "Edit manufacturer part" %}'>
<span class='fas fa-edit icon-green'/>
</button>
{% if roles.purchase_order.delete %}
<button type='button' class='btn btn-default btn-glyph' id='delete-part' title='{% trans "Delete manufacturer part" %}'>
<span class='fas fa-trash-alt icon-red'/>
</button>
{% endif %}
</div>
</div>
{% endif %}
{% endblock %}
{% block page_details %}
<h4>{% trans "Manufacturer Part Details" %}</h4>
<table class="table table-striped table-condensed">
<col width='25'>
<tr>
<td><span class='fas fa-shapes'></span></td>
<td>{% trans "Internal Part" %}</td>
<td>
{% if part.part %}
<a href="{% url 'part-manufacturers' part.part.id %}">{{ part.part.full_name }}</a>
{% endif %}
</td>
</tr>
{% if part.description %}
<tr>
<td></td>
<td>{% trans "Description" %}</td>
<td>{{ part.description }}</td>
</tr>
{% endif %}
{% if part.link %}
<tr>
<td><span class='fas fa-link'></span></td>
<td>{% trans "External Link" %}</td>
<td><a href="{{ part.link }}">{{ part.link }}</a></td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-industry'></span></td>
<td>{% trans "Manufacturer" %}</td>
<td><a href="{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a></td></tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "MPN" %}</td>
<td>{{ part.MPN }}</td>
</tr>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
enableNavbar({
label: 'manufacturer-part',
toggleId: '#manufacturer-part-menu-toggle'
})
$('#order-part, #order-part2').click(function() {
launchModalForm(
"{% url 'order-parts' %}",
{
data: {
part: {{ part.part.id }},
},
reload: true,
},
);
});
$('#edit-part').click(function () {
launchModalForm(
"{% url 'manufacturer-part-edit' part.id %}",
{
reload: true
}
);
});
$('#delete-part').click(function() {
launchModalForm(
"{% url 'manufacturer-part-delete' %}?part={{ part.id }}",
{
redirect: "{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}"
}
);
});
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
{{ block.super }}
{% if part %}
<div class='alert alert-block alert-info'>
{% include "hover_image.html" with image=part.image %}
{{ part.full_name}}
<br>
<i>{{ part.description }}</i>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% block pre_form_content %}
<div class='alert alert-block alert-warning'>
{% trans "Are you sure you want to delete the following Manufacturer Parts?" %}
</div>
{% for part in parts %}
{% endfor %}
{% endblock %}
{% block form_data %}
{% for part in parts %}
<table class='table table-striped table-condensed'>
<tr>
<input type='hidden' name='manufacturer-part-{{ part.id}}' value='manufacturer-part-{{ part.id }}'/>
<td>
{% include "hover_image.html" with image=part.part.image %}
{{ part.part.full_name }}
</td>
<td>
{% include "hover_image.html" with image=part.manufacturer.image %}
{{ part.manufacturer.name }}
</td>
<td>
{{ part.MPN }}
</td>
</tr>
</table>
{% if part.supplier_parts.all|length > 0 %}
<div class='alert alert-block alert-danger'>
<p>There are {{ part.supplier_parts.all|length }} suppliers defined for this manufacturer part. If you delete it, the following supplier parts will also be deleted:
</p>
<ul class='list-group' style='margin-top:10px'>
{% for spart in part.supplier_parts.all %}
<li class='list-group-item'>{{ spart.supplier.name }} - {{ spart.SKU }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "company/manufacturer_part_base.html" %}
{% load static %}
{% load i18n %}
{% block menubar %}
{% include "company/manufacturer_part_navbar.html" with tab='details' %}
{% endblock %}
{% block heading %}
{% trans "Manufacturer Part Details" %}
{% endblock %}
{% block details %}
<table class="table table-striped table-condensed">
<tr>
<td>{% trans "Internal Part" %}</td>
<td>
{% if part.part %}
<a href="{% url 'part-manufacturers' part.part.id %}">{{ part.part.full_name }}</a>
{% endif %}
</td>
</tr>
<tr><td>{% trans "Manufacturer" %}</td><td><a href="{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a></td></tr>
<tr><td>{% trans "MPN" %}</td><td>{{ part.MPN }}</tr></tr>
{% if part.link %}
<tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr>
{% endif %}
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
{% endblock %}

View File

@ -0,0 +1,34 @@
{% load i18n %}
<ul class='list-group'>
<li class='list-group-item'>
<a href='#' id='manufacturer-part-menu-toggle'>
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
</a>
</li>
<li class='list-group-item {% if tab == "suppliers" %}active{% endif %}' title='{% trans "Supplier Parts" %}'>
<a href='{% url "manufacturer-part-suppliers" part.id %}'>
<span class='fas fa-building'></span>
{% trans "Suppliers" %}
</a>
</li>
{% comment "for later" %}
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Manufacturer Part Stock" %}'>
<a href='{% url "manufacturer-part-stock" part.id %}'>
<span class='fas fa-boxes'></span>
{% trans "Stock" %}
</a>
</li>
<li class='list-group-item {% if tab == "orders" %}active{% endif %}' title='{% trans "Manufacturer Part Orders" %}'>
<a href='{% url "manufacturer-part-orders" part.id %}'>
<span class='fas fa-shopping-cart'></span>
{% trans "Orders" %}
</a>
</li>
{% endcomment %}
</ul>

View File

@ -0,0 +1,89 @@
{% extends "company/manufacturer_part_base.html" %}
{% load static %}
{% load i18n %}
{% block menubar %}
{% include "company/manufacturer_part_navbar.html" with tab='suppliers' %}
{% endblock %}
{% block heading %}
{% trans "Supplier Parts" %}
{% endblock %}
{% block details %}
<div id='button-toolbar'>
<div class='btn-group'>
<button class="btn btn-success" id='supplier-create'>
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
</button>
<div id='opt-dropdown' class="btn-group">
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'>{% trans "Delete" %}</a></li>
</ul>
</div>
</div>
</div>
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$('#supplier-create').click(function () {
launchModalForm(
"{% url 'supplier-part-create' %}",
{
reload: true,
data: {
manufacturer_part: {{ part.id }}
},
secondary: [
{
field: 'supplier',
label: '{% trans "New Supplier" %}',
title: '{% trans "Create new supplier" %}',
url: "{% url 'supplier-create' %}"
},
]
});
});
$("#supplier-part-delete").click(function() {
var selections = $("#supplier-table").bootstrapTable("getSelections");
var parts = [];
selections.forEach(function(item) {
parts.push(item.pk);
});
launchModalForm("{% url 'supplier-part-delete' %}", {
data: {
parts: parts,
},
reload: true,
});
});
loadSupplierPartTable(
"#supplier-table",
"{% url 'api-supplier-part-list' %}",
{
params: {
part: {{ part.part.id }},
manufacturer_part: {{ part.id }},
part_detail: false,
supplier_detail: true,
manufacturer_detail: false,
},
}
);
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
{% endblock %}

View File

@ -16,14 +16,25 @@
</a> </a>
</li> </li>
{% if company.is_supplier or company.is_manufacturer %} {% if company.is_manufacturer %}
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Supplied Parts" %}'> <li class='list-group-item {% if tab == "manufacturer_parts" %}active{% endif %}' title='{% trans "Manufactured Parts" %}'>
<a href='{% url "company-detail-parts" company.id %}'> <a href='{% url "company-detail-manufacturer-parts" company.id %}'>
<span class='fas fa-shapes'></span> <span class='fas fa-industry'></span>
{% trans "Parts" %} {% trans "Manufactured Parts" %}
</a> </a>
</li> </li>
{% endif %}
{% if company.is_supplier or company.is_manufacturer %}
<li class='list-group-item {% if tab == "supplier_parts" %}active{% endif %}' title='{% trans "Supplied Parts" %}'>
<a href='{% url "company-detail-supplier-parts" company.id %}'>
<span class='fas fa-building'></span>
{% trans "Supplied Parts" %}
</a>
</li>
{% endif %}
{% if company.is_manufacturer or company.is_supplier %}
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Stock Items" %}'> <li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Stock Items" %}'>
<a href='{% url "company-detail-stock" company.id %}'> <a href='{% url "company-detail-stock" company.id %}'>
<span class='fas fa-boxes'></span> <span class='fas fa-boxes'></span>

View File

@ -81,23 +81,24 @@ src="{% static 'img/blank_image.png' %}"
<tr> <tr>
<td><span class='fas fa-building'></span></td> <td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td> <td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr> <td><a href="{% url 'company-detail-supplier-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<tr> <tr>
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td>{% trans "SKU" %}</td> <td>{% trans "SKU" %}</td>
<td>{{ part.SKU }}</tr> <td>{{ part.SKU }}</tr>
</tr> </tr>
{% if part.manufacturer %} {% if part.manufacturer_part.manufacturer %}
<tr> <tr>
<td><span class='fas fa-industry'></span></td> <td><span class='fas fa-industry'></span></td>
<td>{% trans "Manufacturer" %}</td> <td>{% trans "Manufacturer" %}</td>
<td><a href="{% url 'company-detail-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a></td></tr> <td><a href="{% url 'company-detail-manufacturer-parts' part.manufacturer_part.manufacturer.id %}">{{ part.manufacturer_part.manufacturer.name }}</a></td>
</tr>
{% endif %} {% endif %}
{% if part.MPN %} {% if part.manufacturer_part.MPN %}
<tr> <tr>
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td>{% trans "MPN" %}</td> <td>{% trans "MPN" %}</td>
<td>{{ part.MPN }}</td> <td><a href="{% url 'manufacturer-part-detail' part.manufacturer_part.id %}">{{ part.manufacturer_part.MPN }}</a></td>
</tr> </tr>
{% endif %} {% endif %}
{% if part.packaging %} {% if part.packaging %}
@ -150,7 +151,7 @@ $('#delete-part').click(function() {
launchModalForm( launchModalForm(
"{% url 'supplier-part-delete' %}?part={{ part.id }}", "{% url 'supplier-part-delete' %}?part={{ part.id }}",
{ {
redirect: "{% url 'company-detail-parts' part.supplier.id %}" redirect: "{% url 'company-detail-supplier-parts' part.supplier.id %}"
} }
); );
}); });

View File

@ -13,13 +13,16 @@
<tr> <tr>
<input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/> <input type='hidden' name='supplier-part-{{ part.id}}' value='supplier-part-{{ part.id }}'/>
<td>
{% include "hover_image.html" with image=part.part.image %}
{{ part.part.full_name }}
</td>
<td> <td>
{% include "hover_image.html" with image=part.supplier.image %} {% include "hover_image.html" with image=part.supplier.image %}
{{ part.supplier.name }} {{ part.supplier.name }}
</td> </td>
<td> <td>
{% include "hover_image.html" with image=part.part.image %} {{ part.SKU }}
{{ part.part.full_name }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -3,7 +3,7 @@
{% load i18n %} {% load i18n %}
{% block menubar %} {% block menubar %}
{% include "company/part_navbar.html" with tab='details' %} {% include "company/supplier_part_navbar.html" with tab='details' %}
{% endblock %} {% endblock %}
{% block heading %} {% block heading %}
@ -22,7 +22,7 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr><td>{% trans "Supplier" %}</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr> <tr><td>{% trans "Supplier" %}</td><td><a href="{% url 'company-detail-supplier-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<tr><td>{% trans "SKU" %}</td><td>{{ part.SKU }}</tr></tr> <tr><td>{% trans "SKU" %}</td><td>{{ part.SKU }}</tr></tr>
{% if part.link %} {% if part.link %}
<tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr> <tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr>

View File

@ -1,4 +1,5 @@
{% load i18n %} {% load i18n %}
{% load inventree_extras %}
<ul class='list-group'> <ul class='list-group'>

View File

@ -3,7 +3,7 @@
{% load i18n %} {% load i18n %}
{% block menubar %} {% block menubar %}
{% include "company/part_navbar.html" with tab='orders' %} {% include "company/supplier_part_navbar.html" with tab='orders' %}
{% endblock %} {% endblock %}
{% block heading %} {% block heading %}

View File

@ -4,7 +4,7 @@
{% load inventree_extras %} {% load inventree_extras %}
{% block menubar %} {% block menubar %}
{% include "company/part_navbar.html" with tab='pricing' %} {% include "company/supplier_part_navbar.html" with tab='pricing' %}
{% endblock %} {% endblock %}
{% block heading %} {% block heading %}

View File

@ -3,7 +3,7 @@
{% load i18n %} {% load i18n %}
{% block menubar %} {% block menubar %}
{% include "company/part_navbar.html" with tab='stock' %} {% include "company/supplier_part_navbar.html" with tab='stock' %}
{% endblock %} {% endblock %}
{% block heading %} {% block heading %}

View File

@ -27,7 +27,7 @@ class CompanyTest(InvenTreeAPITestCase):
def test_company_list(self): def test_company_list(self):
url = reverse('api-company-list') url = reverse('api-company-list')
# There should be two companies # There should be three companies
response = self.get(url) response = self.get(url)
self.assertEqual(len(response.data), 3) self.assertEqual(len(response.data), 3)
@ -62,3 +62,90 @@ class CompanyTest(InvenTreeAPITestCase):
data = {'search': 'cup'} data = {'search': 'cup'}
response = self.get(url, data) response = self.get(url, data)
self.assertEqual(len(response.data), 2) self.assertEqual(len(response.data), 2)
class ManufacturerTest(InvenTreeAPITestCase):
"""
Series of tests for the Manufacturer DRF API
"""
fixtures = [
'category',
'part',
'location',
'company',
'manufacturer_part',
]
roles = [
'part.add',
'part.change',
]
def test_manufacturer_part_list(self):
url = reverse('api-manufacturer-part-list')
# There should be three manufacturer parts
response = self.get(url)
self.assertEqual(len(response.data), 3)
# Create manufacturer part
data = {
'part': 1,
'manufacturer': 7,
'MPN': 'MPN_TEST',
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['MPN'], 'MPN_TEST')
# Filter by manufacturer
data = {'company': 7}
response = self.get(url, data)
self.assertEqual(len(response.data), 3)
# Filter by part
data = {'part': 5}
response = self.get(url, data)
self.assertEqual(len(response.data), 2)
def test_manufacturer_part_detail(self):
url = reverse('api-manufacturer-part-detail', kwargs={'pk': 1})
response = self.get(url)
self.assertEqual(response.data['MPN'], 'MPN123')
# Change the MPN
data = {
'MPN': 'MPN-TEST-123',
}
response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['MPN'], 'MPN-TEST-123')
def test_manufacturer_part_search(self):
# Test search functionality in manufacturer list
url = reverse('api-manufacturer-part-list')
data = {'search': 'MPN'}
response = self.get(url, data)
self.assertEqual(len(response.data), 3)
def test_supplier_part_create(self):
url = reverse('api-supplier-part-list')
# Create supplier part
data = {
'part': 1,
'supplier': 1,
'SKU': 'SKU_TEST',
'manufacturer': 7,
'MPN': 'PART_NUMBER',
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# Check manufacturer part
manufacturer_part_id = int(response.data['manufacturer_part']['pk'])
url = reverse('api-manufacturer-part-detail', kwargs={'pk': manufacturer_part_id})
response = self.get(url)
self.assertEqual(response.data['MPN'], 'PART_NUMBER')

View File

@ -79,7 +79,7 @@ class TestManufacturerField(MigratorTestCase):
part=part, part=part,
supplier=supplier, supplier=supplier,
SKU='SCREW.002', SKU='SCREW.002',
manufacturer_name='Zero Corp' manufacturer_name='Zero Corp',
) )
self.assertEqual(Company.objects.count(), 1) self.assertEqual(Company.objects.count(), 1)
@ -107,6 +107,136 @@ class TestManufacturerField(MigratorTestCase):
self.assertEqual(part.manufacturer.name, 'ACME') self.assertEqual(part.manufacturer.name, 'ACME')
class TestManufacturerPart(MigratorTestCase):
"""
Tests for migration 0034-0037 which added and transitioned to the ManufacturerPart model
"""
migrate_from = ('company', '0033_auto_20210410_1528')
migrate_to = ('company', '0037_supplierpart_update_3')
def prepare(self):
"""
Prepare the database by adding some test data 'before' the change:
- Part object
- Company object (supplier)
- SupplierPart object
"""
Part = self.old_state.apps.get_model('part', 'part')
Company = self.old_state.apps.get_model('company', 'company')
SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
# Create an initial part
part = Part.objects.create(
name='CAP CER 0.1UF 10V X5R 0402',
description='CAP CER 0.1UF 10V X5R 0402',
purchaseable=True,
level=0,
tree_id=0,
lft=0,
rght=0,
)
# Create a manufacturer
manufacturer = Company.objects.create(
name='Murata',
description='Makes capacitors',
is_manufacturer=True,
is_supplier=False,
is_customer=False,
)
# Create suppliers
supplier_1 = Company.objects.create(
name='Digi-Key',
description='A supplier of components',
is_manufacturer=False,
is_supplier=True,
is_customer=False,
)
supplier_2 = Company.objects.create(
name='Mouser',
description='We sell components',
is_manufacturer=False,
is_supplier=True,
is_customer=False,
)
# Add some SupplierPart objects
SupplierPart.objects.create(
part=part,
supplier=supplier_1,
SKU='DK-MUR-CAP-123456-ND',
manufacturer=manufacturer,
MPN='MUR-CAP-123456',
)
SupplierPart.objects.create(
part=part,
supplier=supplier_1,
SKU='DK-MUR-CAP-987654-ND',
manufacturer=manufacturer,
MPN='MUR-CAP-987654',
)
SupplierPart.objects.create(
part=part,
supplier=supplier_2,
SKU='CAP-CER-01UF',
manufacturer=manufacturer,
MPN='MUR-CAP-123456',
)
# No MPN
SupplierPart.objects.create(
part=part,
supplier=supplier_2,
SKU='CAP-CER-01UF-1',
manufacturer=manufacturer,
)
# No Manufacturer
SupplierPart.objects.create(
part=part,
supplier=supplier_2,
SKU='CAP-CER-01UF-2',
MPN='MUR-CAP-123456',
)
# No Manufacturer data
SupplierPart.objects.create(
part=part,
supplier=supplier_2,
SKU='CAP-CER-01UF-3',
)
def test_manufacturer_part_objects(self):
"""
Test that the new companies have been created successfully
"""
# Check on the SupplierPart objects
SupplierPart = self.new_state.apps.get_model('company', 'supplierpart')
supplier_parts = SupplierPart.objects.all()
self.assertEqual(supplier_parts.count(), 6)
supplier_parts = SupplierPart.objects.filter(supplier__name='Mouser')
self.assertEqual(supplier_parts.count(), 4)
# Check on the ManufacturerPart objects
ManufacturerPart = self.new_state.apps.get_model('company', 'manufacturerpart')
manufacturer_parts = ManufacturerPart.objects.all()
self.assertEqual(manufacturer_parts.count(), 4)
manufacturer_part = manufacturer_parts.first()
self.assertEqual(manufacturer_part.MPN, 'MUR-CAP-123456')
class TestCurrencyMigration(MigratorTestCase): class TestCurrencyMigration(MigratorTestCase):
""" """
Tests for upgrade from basic currency support to django-money Tests for upgrade from basic currency support to django-money

View File

@ -10,6 +10,7 @@ from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from .models import ManufacturerPart
from .models import SupplierPart from .models import SupplierPart
@ -20,6 +21,7 @@ class CompanyViewTestBase(TestCase):
'part', 'part',
'location', 'location',
'company', 'company',
'manufacturer_part',
'supplier_part', 'supplier_part',
] ]
@ -200,3 +202,105 @@ class CompanyViewTest(CompanyViewTestBase):
response = self.client.get(reverse('customer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(reverse('customer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, 'Create new Customer') self.assertContains(response, 'Create new Customer')
class ManufacturerPartViewTests(CompanyViewTestBase):
"""
Tests for the ManufacturerPart views.
"""
def test_manufacturer_part_create(self):
"""
Test the ManufacturerPartCreate view.
"""
url = reverse('manufacturer-part-create')
# First check that we can GET the form
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# How many manufaturer parts are already in the database?
n = ManufacturerPart.objects.all().count()
data = {
'part': 1,
'manufacturer': 6,
}
# MPN is required! (form should fail)
(response, errors) = self.post(url, data, valid=False)
self.assertIsNotNone(errors.get('MPN', None))
data['MPN'] = 'TEST-ME-123'
(response, errors) = self.post(url, data, valid=True)
# Check that the ManufacturerPart was created!
self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
# Try to create duplicate ManufacturerPart
(response, errors) = self.post(url, data, valid=False)
self.assertIsNotNone(errors.get('__all__', None))
# Check that the ManufacturerPart count stayed the same
self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
def test_supplier_part_create(self):
"""
Test that the SupplierPartCreate view creates Manufacturer Part.
"""
url = reverse('supplier-part-create')
# First check that we can GET the form
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# How many manufacturer parts are already in the database?
n = ManufacturerPart.objects.all().count()
data = {
'part': 1,
'supplier': 1,
'SKU': 'SKU_TEST',
'manufacturer': 6,
'MPN': 'MPN_TEST',
}
(response, errors) = self.post(url, data, valid=True)
# Check that the ManufacturerPart was created!
self.assertEqual(n + 1, ManufacturerPart.objects.all().count())
def test_manufacturer_part_delete(self):
"""
Test the ManufacturerPartDelete view
"""
url = reverse('manufacturer-part-delete')
# Get form using 'part' argument
response = self.client.get(url, {'part': '2'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# POST to delete manufacturer part
n = ManufacturerPart.objects.count()
m = SupplierPart.objects.count()
response = self.client.post(
url,
{
'manufacturer-part-2': 'manufacturer-part-2',
'confirm_delete': True
},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
# Check that the ManufacturerPart was deleted
self.assertEqual(n - 1, ManufacturerPart.objects.count())
# Check that the SupplierParts were deleted
self.assertEqual(m - 2, SupplierPart.objects.count())

View File

@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
import os import os
from .models import Company, Contact, SupplierPart from .models import Company, Contact, ManufacturerPart, SupplierPart
from .models import rename_company_image from .models import rename_company_image
from part.models import Part from part.models import Part
@ -22,6 +22,7 @@ class CompanySimpleTest(TestCase):
'part', 'part',
'location', 'location',
'bom', 'bom',
'manufacturer_part',
'supplier_part', 'supplier_part',
'price_breaks', 'price_breaks',
] ]
@ -74,10 +75,10 @@ class CompanySimpleTest(TestCase):
self.assertEqual(acme.supplied_part_count, 4) self.assertEqual(acme.supplied_part_count, 4)
self.assertTrue(appel.has_parts) self.assertTrue(appel.has_parts)
self.assertEqual(appel.supplied_part_count, 2) self.assertEqual(appel.supplied_part_count, 3)
self.assertTrue(zerg.has_parts) self.assertTrue(zerg.has_parts)
self.assertEqual(zerg.supplied_part_count, 1) self.assertEqual(zerg.supplied_part_count, 2)
def test_price_breaks(self): def test_price_breaks(self):
@ -166,3 +167,53 @@ class ContactSimpleTest(TestCase):
# Remove the parent company # Remove the parent company
Company.objects.get(pk=self.c.pk).delete() Company.objects.get(pk=self.c.pk).delete()
self.assertEqual(Contact.objects.count(), 0) self.assertEqual(Contact.objects.count(), 0)
class ManufacturerPartSimpleTest(TestCase):
fixtures = [
'category',
'company',
'location',
'part',
'manufacturer_part',
]
def setUp(self):
# Create a manufacturer part
self.part = Part.objects.get(pk=1)
manufacturer = Company.objects.get(pk=1)
self.mp = ManufacturerPart.create(
part=self.part,
manufacturer=manufacturer,
mpn='PART_NUMBER',
description='THIS IS A MANUFACTURER PART',
)
# Create a supplier part
supplier = Company.objects.get(pk=5)
supplier_part = SupplierPart.objects.create(
part=self.part,
supplier=supplier,
SKU='SKU_TEST',
)
kwargs = {
'manufacturer': manufacturer.id,
'MPN': 'MPN_TEST',
}
supplier_part.save(**kwargs)
def test_exists(self):
self.assertEqual(ManufacturerPart.objects.count(), 5)
# Check that manufacturer part was created from supplier part creation
manufacturer_parts = ManufacturerPart.objects.filter(manufacturer=1)
self.assertEqual(manufacturer_parts.count(), 2)
def test_delete(self):
# Remove a part
Part.objects.get(pk=self.part.id).delete()
# Check that ManufacturerPart was deleted
self.assertEqual(ManufacturerPart.objects.count(), 3)

View File

@ -13,7 +13,8 @@ company_detail_urls = [
# url(r'orders/?', views.CompanyDetail.as_view(template_name='company/orders.html'), name='company-detail-orders'), # url(r'orders/?', views.CompanyDetail.as_view(template_name='company/orders.html'), name='company-detail-orders'),
url(r'^parts/', views.CompanyDetail.as_view(template_name='company/detail_part.html'), name='company-detail-parts'), url(r'^supplier-parts/', views.CompanyDetail.as_view(template_name='company/detail_supplier_part.html'), name='company-detail-supplier-parts'),
url(r'^manufacturer-parts/', views.CompanyDetail.as_view(template_name='company/detail_manufacturer_part.html'), name='company-detail-manufacturer-parts'),
url(r'^stock/', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'), url(r'^stock/', views.CompanyDetail.as_view(template_name='company/detail_stock.html'), name='company-detail-stock'),
url(r'^purchase-orders/', views.CompanyDetail.as_view(template_name='company/purchase_orders.html'), name='company-detail-purchase-orders'), url(r'^purchase-orders/', views.CompanyDetail.as_view(template_name='company/purchase_orders.html'), name='company-detail-purchase-orders'),
url(r'^assigned-stock/', views.CompanyDetail.as_view(template_name='company/assigned_stock.html'), name='company-detail-assigned-stock'), url(r'^assigned-stock/', views.CompanyDetail.as_view(template_name='company/assigned_stock.html'), name='company-detail-assigned-stock'),
@ -52,9 +53,26 @@ price_break_urls = [
url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'), url(r'^(?P<pk>\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'),
] ]
manufacturer_part_detail_urls = [
url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'),
url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'),
url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'),
]
manufacturer_part_urls = [
url(r'^new/?', views.ManufacturerPartCreate.as_view(), name='manufacturer-part-create'),
url(r'delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'),
url(r'^(?P<pk>\d+)/', include(manufacturer_part_detail_urls)),
]
supplier_part_detail_urls = [ supplier_part_detail_urls = [
url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'), url(r'^edit/?', views.SupplierPartEdit.as_view(), name='supplier-part-edit'),
url(r'^manufacturers/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_manufacturers.html'), name='supplier-part-manufacturers'),
url(r'^pricing/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_pricing.html'), name='supplier-part-pricing'), url(r'^pricing/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_pricing.html'), name='supplier-part-pricing'),
url(r'^orders/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_orders.html'), name='supplier-part-orders'), url(r'^orders/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_orders.html'), name='supplier-part-orders'),
url(r'^stock/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_stock.html'), name='supplier-part-stock'), url(r'^stock/', views.SupplierPartDetail.as_view(template_name='company/supplier_part_stock.html'), name='supplier-part-stock'),

View File

@ -24,6 +24,7 @@ from InvenTree.helpers import str2bool
from InvenTree.views import InvenTreeRoleMixin from InvenTree.views import InvenTreeRoleMixin
from .models import Company from .models import Company
from .models import ManufacturerPart
from .models import SupplierPart from .models import SupplierPart
from .models import SupplierPriceBreak from .models import SupplierPriceBreak
@ -31,6 +32,7 @@ from part.models import Part
from .forms import EditCompanyForm from .forms import EditCompanyForm
from .forms import CompanyImageForm from .forms import CompanyImageForm
from .forms import EditManufacturerPartForm
from .forms import EditSupplierPartForm from .forms import EditSupplierPartForm
from .forms import EditPriceBreakForm from .forms import EditPriceBreakForm
from .forms import CompanyImageDownloadForm from .forms import CompanyImageDownloadForm
@ -331,6 +333,177 @@ class CompanyDelete(AjaxDeleteView):
} }
class ManufacturerPartDetail(DetailView):
""" Detail view for ManufacturerPart """
model = ManufacturerPart
template_name = 'company/manufacturer_part_detail.html'
context_object_name = 'part'
queryset = ManufacturerPart.objects.all()
permission_required = 'purchase_order.view'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
return ctx
class ManufacturerPartEdit(AjaxUpdateView):
""" Update view for editing ManufacturerPart """
model = ManufacturerPart
context_object_name = 'part'
form_class = EditManufacturerPartForm
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Manufacturer Part')
class ManufacturerPartCreate(AjaxCreateView):
""" Create view for making new ManufacturerPart """
model = ManufacturerPart
form_class = EditManufacturerPartForm
ajax_template_name = 'company/manufacturer_part_create.html'
ajax_form_title = _('Create New Manufacturer Part')
context_object_name = 'part'
def get_context_data(self):
"""
Supply context data to the form
"""
ctx = super().get_context_data()
# Add 'part' object
form = self.get_form()
part = form['part'].value()
try:
part = Part.objects.get(pk=part)
except (ValueError, Part.DoesNotExist):
part = None
ctx['part'] = part
return ctx
def get_form(self):
""" Create Form instance to create a new ManufacturerPart object.
Hide some fields if they are not appropriate in context
"""
form = super(AjaxCreateView, self).get_form()
if form.initial.get('part', None):
# Hide the part field
form.fields['part'].widget = HiddenInput()
return form
def get_initial(self):
""" Provide initial data for new ManufacturerPart:
- If 'manufacturer_id' provided, pre-fill manufacturer field
- If 'part_id' provided, pre-fill part field
"""
initials = super(ManufacturerPartCreate, self).get_initial().copy()
manufacturer_id = self.get_param('manufacturer')
part_id = self.get_param('part')
if manufacturer_id:
try:
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
except (ValueError, Company.DoesNotExist):
pass
if part_id:
try:
initials['part'] = Part.objects.get(pk=part_id)
except (ValueError, Part.DoesNotExist):
pass
return initials
class ManufacturerPartDelete(AjaxDeleteView):
""" Delete view for removing a ManufacturerPart.
ManufacturerParts can be deleted using a variety of 'selectors'.
- ?part=<pk> -> Delete a single ManufacturerPart object
- ?parts=[] -> Delete a list of ManufacturerPart objects
"""
success_url = '/manufacturer/'
ajax_template_name = 'company/manufacturer_part_delete.html'
ajax_form_title = _('Delete Manufacturer Part')
role_required = 'purchase_order.delete'
parts = []
def get_context_data(self):
ctx = {}
ctx['parts'] = self.parts
return ctx
def get_parts(self):
""" Determine which ManufacturerPart object(s) the user wishes to delete.
"""
self.parts = []
# User passes a single ManufacturerPart ID
if 'part' in self.request.GET:
try:
self.parts.append(ManufacturerPart.objects.get(pk=self.request.GET.get('part')))
except (ValueError, ManufacturerPart.DoesNotExist):
pass
elif 'parts[]' in self.request.GET:
part_id_list = self.request.GET.getlist('parts[]')
self.parts = ManufacturerPart.objects.filter(id__in=part_id_list)
def get(self, request, *args, **kwargs):
self.request = request
self.get_parts()
return self.renderJsonResponse(request, form=self.get_form())
def post(self, request, *args, **kwargs):
""" Handle the POST action for deleting ManufacturerPart object.
"""
self.request = request
self.parts = []
for item in self.request.POST:
if item.startswith('manufacturer-part-'):
pk = item.replace('manufacturer-part-', '')
try:
self.parts.append(ManufacturerPart.objects.get(pk=pk))
except (ValueError, ManufacturerPart.DoesNotExist):
pass
confirm = str2bool(self.request.POST.get('confirm_delete', False))
data = {
'form_valid': confirm,
}
if confirm:
for part in self.parts:
part.delete()
return self.renderJsonResponse(self.request, data=data, form=self.get_form())
class SupplierPartDetail(DetailView): class SupplierPartDetail(DetailView):
""" Detail view for SupplierPart """ """ Detail view for SupplierPart """
model = SupplierPart model = SupplierPart
@ -354,11 +527,25 @@ class SupplierPartEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Supplier Part') ajax_form_title = _('Edit Supplier Part')
def save(self, supplier_part, form, **kwargs):
""" Process ManufacturerPart data """
manufacturer = form.cleaned_data.get('manufacturer', None)
MPN = form.cleaned_data.get('MPN', None)
kwargs = {'manufacturer': manufacturer,
'MPN': MPN,
}
supplier_part.save(**kwargs)
def get_form(self): def get_form(self):
form = super().get_form() form = super().get_form()
supplier_part = self.get_object() supplier_part = self.get_object()
# Hide Manufacturer fields
form.fields['manufacturer'].widget = HiddenInput()
form.fields['MPN'].widget = HiddenInput()
# It appears that hiding a MoneyField fails validation # It appears that hiding a MoneyField fails validation
# Therefore the idea to set the value before hiding # Therefore the idea to set the value before hiding
if form.is_valid(): if form.is_valid():
@ -368,6 +555,19 @@ class SupplierPartEdit(AjaxUpdateView):
return form return form
def get_initial(self):
""" Fetch data from ManufacturerPart """
initials = super(SupplierPartEdit, self).get_initial().copy()
supplier_part = self.get_object()
if supplier_part.manufacturer_part:
initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id
initials['MPN'] = supplier_part.manufacturer_part.MPN
return initials
class SupplierPartCreate(AjaxCreateView): class SupplierPartCreate(AjaxCreateView):
""" Create view for making new SupplierPart """ """ Create view for making new SupplierPart """
@ -415,6 +615,14 @@ class SupplierPartCreate(AjaxCreateView):
# Save the supplier part object # Save the supplier part object
supplier_part = super().save(form) supplier_part = super().save(form)
# Process manufacturer data
manufacturer = form.cleaned_data.get('manufacturer', None)
MPN = form.cleaned_data.get('MPN', None)
kwargs = {'manufacturer': manufacturer,
'MPN': MPN,
}
supplier_part.save(**kwargs)
single_pricing = form.cleaned_data.get('single_pricing', None) single_pricing = form.cleaned_data.get('single_pricing', None)
if single_pricing: if single_pricing:
@ -433,6 +641,12 @@ class SupplierPartCreate(AjaxCreateView):
# Hide the part field # Hide the part field
form.fields['part'].widget = HiddenInput() form.fields['part'].widget = HiddenInput()
if form.initial.get('manufacturer', None):
# Hide the manufacturer field
form.fields['manufacturer'].widget = HiddenInput()
# Hide the MPN field
form.fields['MPN'].widget = HiddenInput()
return form return form
def get_initial(self): def get_initial(self):
@ -446,6 +660,7 @@ class SupplierPartCreate(AjaxCreateView):
manufacturer_id = self.get_param('manufacturer') manufacturer_id = self.get_param('manufacturer')
supplier_id = self.get_param('supplier') supplier_id = self.get_param('supplier')
part_id = self.get_param('part') part_id = self.get_param('part')
manufacturer_part_id = self.get_param('manufacturer_part')
supplier = None supplier = None
@ -461,6 +676,16 @@ class SupplierPartCreate(AjaxCreateView):
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id) initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
except (ValueError, Company.DoesNotExist): except (ValueError, Company.DoesNotExist):
pass pass
if manufacturer_part_id:
try:
# Get ManufacturerPart instance information
manufacturer_part_obj = ManufacturerPart.objects.get(pk=manufacturer_part_id)
initials['part'] = Part.objects.get(pk=manufacturer_part_obj.part.id)
initials['manufacturer'] = manufacturer_part_obj.manufacturer.id
initials['MPN'] = manufacturer_part_obj.MPN
except (ValueError, ManufacturerPart.DoesNotExist, Part.DoesNotExist, Company.DoesNotExist):
pass
if part_id: if part_id:
try: try:
@ -493,7 +718,7 @@ class SupplierPartDelete(AjaxDeleteView):
""" """
success_url = '/supplier/' success_url = '/supplier/'
ajax_template_name = 'company/partdelete.html' ajax_template_name = 'company/supplier_part_delete.html'
ajax_form_title = _('Delete Supplier Part') ajax_form_title = _('Delete Supplier Part')
role_required = 'purchase_order.delete' role_required = 'purchase_order.delete'

View File

@ -253,10 +253,12 @@ class StockItemLabel(LabelTemplate):
'part': stock_item.part, 'part': stock_item.part,
'name': stock_item.part.full_name, 'name': stock_item.part.full_name,
'ipn': stock_item.part.IPN, 'ipn': stock_item.part.IPN,
'revision': stock_item.part.revision,
'quantity': normalize(stock_item.quantity), 'quantity': normalize(stock_item.quantity),
'serial': stock_item.serial, 'serial': stock_item.serial,
'uid': stock_item.uid, 'uid': stock_item.uid,
'qr_data': stock_item.format_barcode(brief=True), 'qr_data': stock_item.format_barcode(brief=True),
'qr_url': stock_item.format_barcode(url=True, request=request),
'tests': stock_item.testResultMap() 'tests': stock_item.testResultMap()
} }

View File

@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 22:07+0000\n" "POT-Creation-Date: 2021-04-15 10:07+0000\n"
"PO-Revision-Date: 2021-03-28 17:47+0200\n" "PO-Revision-Date: 2021-03-28 17:47+0200\n"
"Last-Translator: Andreas Kaiser <kaiser.vocote@gmail.com>, Matthias " "Last-Translator: Andreas Kaiser <kaiser.vocote@gmail.com>, Matthias "
"MAIR<matmair@live.de>\n" "MAIR<matmair@live.de>\n"
@ -190,11 +190,15 @@ msgstr "Polnisch"
msgid "Turkish" msgid "Turkish"
msgstr "Türkisch" msgstr "Türkisch"
#: InvenTree/status.py:57 #: InvenTree/status.py:84
msgid "Background worker check failed" msgid "Background worker check failed"
msgstr "Hintergrund-Prozess-Kontrolle fehlgeschlagen" msgstr "Hintergrund-Prozess-Kontrolle fehlgeschlagen"
#: InvenTree/status.py:60 #: InvenTree/status.py:88
msgid "Email backend not configured"
msgstr ""
#: InvenTree/status.py:91
msgid "InvenTree system health checks failed" msgid "InvenTree system health checks failed"
msgstr "InvenTree Status-Überprüfung fehlgeschlagen" msgstr "InvenTree Status-Überprüfung fehlgeschlagen"
@ -2045,28 +2049,29 @@ msgid "Supplied Parts"
msgstr "Zulieferer-Teile" msgstr "Zulieferer-Teile"
#: company/templates/company/navbar.html:23 #: company/templates/company/navbar.html:23
#: order/templates/order/receive_parts.html:14 part/models.py:322 #: order/templates/order/receive_parts.html:14 part/api.py:40
#: part/templates/part/cat_link.html:7 part/templates/part/category.html:95 #: part/models.py:322 part/templates/part/cat_link.html:7
#: part/templates/part/category.html:95
#: part/templates/part/category_navbar.html:11 #: part/templates/part/category_navbar.html:11
#: part/templates/part/category_navbar.html:14 #: part/templates/part/category_navbar.html:14
#: part/templates/part/category_partlist.html:10 #: part/templates/part/category_partlist.html:10
#: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113 #: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113
#: templates/InvenTree/settings/tabs.html:25 templates/navbar.html:23 #: templates/InvenTree/settings/tabs.html:25 templates/navbar.html:23
#: templates/stats.html:48 templates/stats.html:57 users/models.py:38 #: templates/stats.html:59 templates/stats.html:68 users/models.py:38
msgid "Parts" msgid "Parts"
msgstr "Teile" msgstr "Teile"
#: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33 #: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33
#: stock/templates/stock/location.html:100 #: stock/templates/stock/location.html:100
#: stock/templates/stock/location.html:115 templates/InvenTree/search.html:182 #: stock/templates/stock/location.html:115 templates/InvenTree/search.html:182
#: templates/stats.html:61 templates/stats.html:70 users/models.py:40 #: templates/stats.html:72 templates/stats.html:81 users/models.py:40
msgid "Stock Items" msgid "Stock Items"
msgstr "BestandsObjekte" msgstr "BestandsObjekte"
#: company/templates/company/navbar.html:30 #: company/templates/company/navbar.html:30
#: company/templates/company/part_navbar.html:14 #: company/templates/company/part_navbar.html:14
#: part/templates/part/navbar.html:36 stock/templates/stock/loc_link.html:7 #: part/templates/part/navbar.html:36 stock/api.py:51
#: stock/templates/stock/location.html:29 #: stock/templates/stock/loc_link.html:7 stock/templates/stock/location.html:29
#: stock/templates/stock/stock_app_base.html:9 #: stock/templates/stock/stock_app_base.html:9
#: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180 #: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180
#: templates/InvenTree/search.html:216 #: templates/InvenTree/search.html:216
@ -3274,7 +3279,7 @@ msgstr "Teil-Kategorie"
#: part/models.py:83 part/templates/part/category.html:19 #: part/models.py:83 part/templates/part/category.html:19
#: part/templates/part/category.html:90 part/templates/part/category.html:141 #: part/templates/part/category.html:90 part/templates/part/category.html:141
#: templates/InvenTree/search.html:126 templates/stats.html:52 #: templates/InvenTree/search.html:126 templates/stats.html:63
#: users/models.py:37 #: users/models.py:37
msgid "Part Categories" msgid "Part Categories"
msgstr "Teil-Kategorien" msgstr "Teil-Kategorien"
@ -5333,7 +5338,7 @@ msgid "Stock Details"
msgstr "Objekt-Details" msgstr "Objekt-Details"
#: stock/templates/stock/location.html:110 templates/InvenTree/search.html:263 #: stock/templates/stock/location.html:110 templates/InvenTree/search.html:263
#: templates/stats.html:65 users/models.py:39 #: templates/stats.html:76 users/models.py:39
msgid "Stock Locations" msgid "Stock Locations"
msgstr "Bestand-Lagerorte" msgstr "Bestand-Lagerorte"
@ -6137,6 +6142,14 @@ msgstr "Vorlagenteil"
msgid "Assembled part" msgid "Assembled part"
msgstr "Baugruppe" msgstr "Baugruppe"
#: templates/js/filters.js:167 templates/js/filters.js:397
msgid "true"
msgstr "ja"
#: templates/js/filters.js:171 templates/js/filters.js:398
msgid "false"
msgstr "nein"
#: templates/js/filters.js:193 #: templates/js/filters.js:193
msgid "Select filter" msgid "Select filter"
msgstr "Filter auswählen" msgstr "Filter auswählen"
@ -6470,6 +6483,22 @@ msgstr "Auftrag zugewiesen"
msgid "No stock items matching query" msgid "No stock items matching query"
msgstr "Keine zur Anfrage passenden BestandsObjekte" msgstr "Keine zur Anfrage passenden BestandsObjekte"
#: templates/js/stock.js:357
msgid "items"
msgstr "Teile"
#: templates/js/stock.js:449
#, fuzzy
#| msgid "Batch"
msgid "batches"
msgstr "Los"
#: templates/js/stock.js:476
#, fuzzy
#| msgid "Allocations"
msgid "locations"
msgstr "Zuweisungen"
#: templates/js/stock.js:478 #: templates/js/stock.js:478
msgid "Undefined location" msgid "Undefined location"
msgstr "unbekannter Lagerort" msgstr "unbekannter Lagerort"
@ -6653,7 +6682,7 @@ msgstr "Elemente, die in Produktion sind, anzeigen"
#: templates/js/table_filters.js:144 #: templates/js/table_filters.js:144
msgid "Include Variants" msgid "Include Variants"
msgstr "Varianten hinzufügen" msgstr "Varianten einschließen"
#: templates/js/table_filters.js:145 #: templates/js/table_filters.js:145
msgid "Include stock items for variant parts" msgid "Include stock items for variant parts"
@ -6792,7 +6821,7 @@ msgstr "Barcode scannen"
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr "Admin"
#: templates/navbar.html:73 templates/registration/logout.html:5 #: templates/navbar.html:73
msgid "Logout" msgid "Logout"
msgstr "Ausloggen" msgstr "Ausloggen"
@ -6808,6 +6837,18 @@ msgstr "Über InvenBaum"
msgid "QR data not provided" msgid "QR data not provided"
msgstr "QR Daten nicht angegeben" msgstr "QR Daten nicht angegeben"
#: templates/registration/logged_out.html:50
msgid "You have been logged out"
msgstr "Sie wurden abgemeldet"
#: templates/registration/logged_out.html:51
#: templates/registration/password_reset_complete.html:51
#: templates/registration/password_reset_done.html:58
#, fuzzy
#| msgid "Returned to location"
msgid "Return to login screen"
msgstr "zurück ins Lager"
#: templates/registration/login.html:64 #: templates/registration/login.html:64
msgid "Enter username" msgid "Enter username"
msgstr "Benutzername eingeben" msgstr "Benutzername eingeben"
@ -6820,17 +6861,61 @@ msgstr "Passwort"
msgid "Username / password combination is incorrect" msgid "Username / password combination is incorrect"
msgstr "Benutzername / Passwort Kombination ist falsch" msgstr "Benutzername / Passwort Kombination ist falsch"
#: templates/registration/logout.html:6 #: templates/registration/login.html:95
msgid "You have been logged out" #: templates/registration/password_reset_form.html:51
msgstr "Sie wurden abgemeldet" #, fuzzy
#| msgid "Enter password"
msgid "Forgotten your password?"
msgstr "Passwort eingeben"
#: templates/registration/logout.html:7 #: templates/registration/login.html:95
msgid "Click" msgid "Click here to reset"
msgstr "Klick" msgstr ""
#: templates/registration/logout.html:7 #: templates/registration/password_reset_complete.html:50
msgid "here</a> to log in</p>" #, fuzzy
msgstr "hier</a> zum abmelden</p>" #| msgid "Purchase order completed"
msgid "Password reset complete"
msgstr "Bestellung als vollständig markieren"
#: templates/registration/password_reset_confirm.html:52
#: templates/registration/password_reset_confirm.html:56
#, fuzzy
#| msgid "Change Password"
msgid "Change password"
msgstr "Passwort ändern"
#: templates/registration/password_reset_confirm.html:60
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a new password reset."
msgstr ""
#: templates/registration/password_reset_done.html:51
msgid ""
"We've emailed you instructions for setting your password, if an account "
"exists with the email you entered. You should receive them shortly."
msgstr ""
#: templates/registration/password_reset_done.html:54
msgid ""
"If you don't receive an email, please make sure you've entered the address "
"you registered with, and check your spam folder."
msgstr ""
#: templates/registration/password_reset_form.html:52
#, fuzzy
#| msgid "Contact email address"
msgid "Enter your email address below."
msgstr "Kontakt-Email"
#: templates/registration/password_reset_form.html:53
msgid "An email will be sent with password reset instructions."
msgstr ""
#: templates/registration/password_reset_form.html:58
msgid "Send email"
msgstr ""
#: templates/stats.html:9 #: templates/stats.html:9
msgid "Server" msgid "Server"
@ -6852,17 +6937,25 @@ msgstr "Gesund"
msgid "Issues detected" msgid "Issues detected"
msgstr "Probleme erkannt" msgstr "Probleme erkannt"
#: templates/stats.html:30 #: templates/stats.html:31
msgid "Background Worker" msgid "Background Worker"
msgstr "Hintergrund-Prozess" msgstr "Hintergrund-Prozess"
#: templates/stats.html:33 #: templates/stats.html:34
msgid "Operational" #, fuzzy
msgstr "Betriebsbereit" #| msgid "Background Worker"
msgid "Background worker not running"
msgstr "Hintergrund-Prozess"
#: templates/stats.html:35 #: templates/stats.html:42
msgid "Not running" #, fuzzy
msgstr "Läuft nicht" #| msgid "Part Settings"
msgid "Email Settings"
msgstr "Teil-Einstellungen"
#: templates/stats.html:45
msgid "Email settings not configured"
msgstr ""
#: templates/stock_table.html:14 #: templates/stock_table.html:14
msgid "Export Stock Information" msgid "Export Stock Information"
@ -6980,6 +7073,28 @@ msgstr "Berechtigungen Einträge zu ändern"
msgid "Permission to delete items" msgid "Permission to delete items"
msgstr "Berechtigung Einträge zu löschen" msgstr "Berechtigung Einträge zu löschen"
#, fuzzy
#~| msgid "Part Pricing"
#~ msgid "Stock Pricing"
#~ msgstr "Teilbepreisung"
#, fuzzy
#~| msgid "No pricing information is available for this part."
#~ msgid "No stock pricing history is available for this part."
#~ msgstr "Keine Preise für dieses Teil verfügbar"
#~ msgid "Click"
#~ msgstr "Klick"
#~ msgid "here</a> to log in</p>"
#~ msgstr "hier</a> zum abmelden</p>"
#~ msgid "Operational"
#~ msgstr "Betriebsbereit"
#~ msgid "Not running"
#~ msgstr "Läuft nicht"
#~ msgid "InvenTree server issues detected" #~ msgid "InvenTree server issues detected"
#~ msgstr "InvenTree Server Fehler aufgetreten" #~ msgstr "InvenTree Server Fehler aufgetreten"
@ -7009,9 +7124,6 @@ msgstr "Berechtigung Einträge zu löschen"
#~ msgid "customer" #~ msgid "customer"
#~ msgstr "Kunde" #~ msgstr "Kunde"
#~ msgid "items"
#~ msgstr "Teile"
#~ msgid "Create purchase order" #~ msgid "Create purchase order"
#~ msgstr "Neue Bestellung anlegen" #~ msgstr "Neue Bestellung anlegen"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 22:07+0000\n" "POT-Creation-Date: 2021-04-15 10:07+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -188,11 +188,15 @@ msgstr ""
msgid "Turkish" msgid "Turkish"
msgstr "" msgstr ""
#: InvenTree/status.py:57 #: InvenTree/status.py:84
msgid "Background worker check failed" msgid "Background worker check failed"
msgstr "" msgstr ""
#: InvenTree/status.py:60 #: InvenTree/status.py:88
msgid "Email backend not configured"
msgstr ""
#: InvenTree/status.py:91
msgid "InvenTree system health checks failed" msgid "InvenTree system health checks failed"
msgstr "" msgstr ""
@ -2022,28 +2026,29 @@ msgid "Supplied Parts"
msgstr "" msgstr ""
#: company/templates/company/navbar.html:23 #: company/templates/company/navbar.html:23
#: order/templates/order/receive_parts.html:14 part/models.py:322 #: order/templates/order/receive_parts.html:14 part/api.py:40
#: part/templates/part/cat_link.html:7 part/templates/part/category.html:95 #: part/models.py:322 part/templates/part/cat_link.html:7
#: part/templates/part/category.html:95
#: part/templates/part/category_navbar.html:11 #: part/templates/part/category_navbar.html:11
#: part/templates/part/category_navbar.html:14 #: part/templates/part/category_navbar.html:14
#: part/templates/part/category_partlist.html:10 #: part/templates/part/category_partlist.html:10
#: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113 #: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113
#: templates/InvenTree/settings/tabs.html:25 templates/navbar.html:23 #: templates/InvenTree/settings/tabs.html:25 templates/navbar.html:23
#: templates/stats.html:48 templates/stats.html:57 users/models.py:38 #: templates/stats.html:59 templates/stats.html:68 users/models.py:38
msgid "Parts" msgid "Parts"
msgstr "" msgstr ""
#: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33 #: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33
#: stock/templates/stock/location.html:100 #: stock/templates/stock/location.html:100
#: stock/templates/stock/location.html:115 templates/InvenTree/search.html:182 #: stock/templates/stock/location.html:115 templates/InvenTree/search.html:182
#: templates/stats.html:61 templates/stats.html:70 users/models.py:40 #: templates/stats.html:72 templates/stats.html:81 users/models.py:40
msgid "Stock Items" msgid "Stock Items"
msgstr "" msgstr ""
#: company/templates/company/navbar.html:30 #: company/templates/company/navbar.html:30
#: company/templates/company/part_navbar.html:14 #: company/templates/company/part_navbar.html:14
#: part/templates/part/navbar.html:36 stock/templates/stock/loc_link.html:7 #: part/templates/part/navbar.html:36 stock/api.py:51
#: stock/templates/stock/location.html:29 #: stock/templates/stock/loc_link.html:7 stock/templates/stock/location.html:29
#: stock/templates/stock/stock_app_base.html:9 #: stock/templates/stock/stock_app_base.html:9
#: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180 #: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180
#: templates/InvenTree/search.html:216 #: templates/InvenTree/search.html:216
@ -3242,7 +3247,7 @@ msgstr ""
#: part/models.py:83 part/templates/part/category.html:19 #: part/models.py:83 part/templates/part/category.html:19
#: part/templates/part/category.html:90 part/templates/part/category.html:141 #: part/templates/part/category.html:90 part/templates/part/category.html:141
#: templates/InvenTree/search.html:126 templates/stats.html:52 #: templates/InvenTree/search.html:126 templates/stats.html:63
#: users/models.py:37 #: users/models.py:37
msgid "Part Categories" msgid "Part Categories"
msgstr "" msgstr ""
@ -5266,7 +5271,7 @@ msgid "Stock Details"
msgstr "" msgstr ""
#: stock/templates/stock/location.html:110 templates/InvenTree/search.html:263 #: stock/templates/stock/location.html:110 templates/InvenTree/search.html:263
#: templates/stats.html:65 users/models.py:39 #: templates/stats.html:76 users/models.py:39
msgid "Stock Locations" msgid "Stock Locations"
msgstr "" msgstr ""
@ -6063,6 +6068,14 @@ msgstr ""
msgid "Assembled part" msgid "Assembled part"
msgstr "" msgstr ""
#: templates/js/filters.js:167 templates/js/filters.js:397
msgid "true"
msgstr ""
#: templates/js/filters.js:171 templates/js/filters.js:398
msgid "false"
msgstr ""
#: templates/js/filters.js:193 #: templates/js/filters.js:193
msgid "Select filter" msgid "Select filter"
msgstr "" msgstr ""
@ -6395,6 +6408,18 @@ msgstr ""
msgid "No stock items matching query" msgid "No stock items matching query"
msgstr "" msgstr ""
#: templates/js/stock.js:357
msgid "items"
msgstr ""
#: templates/js/stock.js:449
msgid "batches"
msgstr ""
#: templates/js/stock.js:476
msgid "locations"
msgstr ""
#: templates/js/stock.js:478 #: templates/js/stock.js:478
msgid "Undefined location" msgid "Undefined location"
msgstr "" msgstr ""
@ -6717,7 +6742,7 @@ msgstr ""
msgid "Admin" msgid "Admin"
msgstr "" msgstr ""
#: templates/navbar.html:73 templates/registration/logout.html:5 #: templates/navbar.html:73
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
@ -6733,6 +6758,16 @@ msgstr ""
msgid "QR data not provided" msgid "QR data not provided"
msgstr "" msgstr ""
#: templates/registration/logged_out.html:50
msgid "You have been logged out"
msgstr ""
#: templates/registration/logged_out.html:51
#: templates/registration/password_reset_complete.html:51
#: templates/registration/password_reset_done.html:58
msgid "Return to login screen"
msgstr ""
#: templates/registration/login.html:64 #: templates/registration/login.html:64
msgid "Enter username" msgid "Enter username"
msgstr "" msgstr ""
@ -6745,16 +6780,52 @@ msgstr ""
msgid "Username / password combination is incorrect" msgid "Username / password combination is incorrect"
msgstr "" msgstr ""
#: templates/registration/logout.html:6 #: templates/registration/login.html:95
msgid "You have been logged out" #: templates/registration/password_reset_form.html:51
msgid "Forgotten your password?"
msgstr "" msgstr ""
#: templates/registration/logout.html:7 #: templates/registration/login.html:95
msgid "Click" msgid "Click here to reset"
msgstr "" msgstr ""
#: templates/registration/logout.html:7 #: templates/registration/password_reset_complete.html:50
msgid "here</a> to log in</p>" msgid "Password reset complete"
msgstr ""
#: templates/registration/password_reset_confirm.html:52
#: templates/registration/password_reset_confirm.html:56
msgid "Change password"
msgstr ""
#: templates/registration/password_reset_confirm.html:60
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a new password reset."
msgstr ""
#: templates/registration/password_reset_done.html:51
msgid ""
"We've emailed you instructions for setting your password, if an account "
"exists with the email you entered. You should receive them shortly."
msgstr ""
#: templates/registration/password_reset_done.html:54
msgid ""
"If you don't receive an email, please make sure you've entered the address "
"you registered with, and check your spam folder."
msgstr ""
#: templates/registration/password_reset_form.html:52
msgid "Enter your email address below."
msgstr ""
#: templates/registration/password_reset_form.html:53
msgid "An email will be sent with password reset instructions."
msgstr ""
#: templates/registration/password_reset_form.html:58
msgid "Send email"
msgstr "" msgstr ""
#: templates/stats.html:9 #: templates/stats.html:9
@ -6777,16 +6848,20 @@ msgstr ""
msgid "Issues detected" msgid "Issues detected"
msgstr "" msgstr ""
#: templates/stats.html:30 #: templates/stats.html:31
msgid "Background Worker" msgid "Background Worker"
msgstr "" msgstr ""
#: templates/stats.html:33 #: templates/stats.html:34
msgid "Operational" msgid "Background worker not running"
msgstr "" msgstr ""
#: templates/stats.html:35 #: templates/stats.html:42
msgid "Not running" msgid "Email Settings"
msgstr ""
#: templates/stats.html:45
msgid "Email settings not configured"
msgstr "" msgstr ""
#: templates/stock_table.html:14 #: templates/stock_table.html:14

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 22:07+0000\n" "POT-Creation-Date: 2021-04-15 10:07+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -188,11 +188,15 @@ msgstr ""
msgid "Turkish" msgid "Turkish"
msgstr "" msgstr ""
#: InvenTree/status.py:57 #: InvenTree/status.py:84
msgid "Background worker check failed" msgid "Background worker check failed"
msgstr "" msgstr ""
#: InvenTree/status.py:60 #: InvenTree/status.py:88
msgid "Email backend not configured"
msgstr ""
#: InvenTree/status.py:91
msgid "InvenTree system health checks failed" msgid "InvenTree system health checks failed"
msgstr "" msgstr ""
@ -2022,28 +2026,29 @@ msgid "Supplied Parts"
msgstr "" msgstr ""
#: company/templates/company/navbar.html:23 #: company/templates/company/navbar.html:23
#: order/templates/order/receive_parts.html:14 part/models.py:322 #: order/templates/order/receive_parts.html:14 part/api.py:40
#: part/templates/part/cat_link.html:7 part/templates/part/category.html:95 #: part/models.py:322 part/templates/part/cat_link.html:7
#: part/templates/part/category.html:95
#: part/templates/part/category_navbar.html:11 #: part/templates/part/category_navbar.html:11
#: part/templates/part/category_navbar.html:14 #: part/templates/part/category_navbar.html:14
#: part/templates/part/category_partlist.html:10 #: part/templates/part/category_partlist.html:10
#: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113 #: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113
#: templates/InvenTree/settings/tabs.html:25 templates/navbar.html:23 #: templates/InvenTree/settings/tabs.html:25 templates/navbar.html:23
#: templates/stats.html:48 templates/stats.html:57 users/models.py:38 #: templates/stats.html:59 templates/stats.html:68 users/models.py:38
msgid "Parts" msgid "Parts"
msgstr "" msgstr ""
#: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33 #: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33
#: stock/templates/stock/location.html:100 #: stock/templates/stock/location.html:100
#: stock/templates/stock/location.html:115 templates/InvenTree/search.html:182 #: stock/templates/stock/location.html:115 templates/InvenTree/search.html:182
#: templates/stats.html:61 templates/stats.html:70 users/models.py:40 #: templates/stats.html:72 templates/stats.html:81 users/models.py:40
msgid "Stock Items" msgid "Stock Items"
msgstr "" msgstr ""
#: company/templates/company/navbar.html:30 #: company/templates/company/navbar.html:30
#: company/templates/company/part_navbar.html:14 #: company/templates/company/part_navbar.html:14
#: part/templates/part/navbar.html:36 stock/templates/stock/loc_link.html:7 #: part/templates/part/navbar.html:36 stock/api.py:51
#: stock/templates/stock/location.html:29 #: stock/templates/stock/loc_link.html:7 stock/templates/stock/location.html:29
#: stock/templates/stock/stock_app_base.html:9 #: stock/templates/stock/stock_app_base.html:9
#: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180 #: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180
#: templates/InvenTree/search.html:216 #: templates/InvenTree/search.html:216
@ -3242,7 +3247,7 @@ msgstr ""
#: part/models.py:83 part/templates/part/category.html:19 #: part/models.py:83 part/templates/part/category.html:19
#: part/templates/part/category.html:90 part/templates/part/category.html:141 #: part/templates/part/category.html:90 part/templates/part/category.html:141
#: templates/InvenTree/search.html:126 templates/stats.html:52 #: templates/InvenTree/search.html:126 templates/stats.html:63
#: users/models.py:37 #: users/models.py:37
msgid "Part Categories" msgid "Part Categories"
msgstr "" msgstr ""
@ -5266,7 +5271,7 @@ msgid "Stock Details"
msgstr "" msgstr ""
#: stock/templates/stock/location.html:110 templates/InvenTree/search.html:263 #: stock/templates/stock/location.html:110 templates/InvenTree/search.html:263
#: templates/stats.html:65 users/models.py:39 #: templates/stats.html:76 users/models.py:39
msgid "Stock Locations" msgid "Stock Locations"
msgstr "" msgstr ""
@ -6063,6 +6068,14 @@ msgstr ""
msgid "Assembled part" msgid "Assembled part"
msgstr "" msgstr ""
#: templates/js/filters.js:167 templates/js/filters.js:397
msgid "true"
msgstr ""
#: templates/js/filters.js:171 templates/js/filters.js:398
msgid "false"
msgstr ""
#: templates/js/filters.js:193 #: templates/js/filters.js:193
msgid "Select filter" msgid "Select filter"
msgstr "" msgstr ""
@ -6395,6 +6408,18 @@ msgstr ""
msgid "No stock items matching query" msgid "No stock items matching query"
msgstr "" msgstr ""
#: templates/js/stock.js:357
msgid "items"
msgstr ""
#: templates/js/stock.js:449
msgid "batches"
msgstr ""
#: templates/js/stock.js:476
msgid "locations"
msgstr ""
#: templates/js/stock.js:478 #: templates/js/stock.js:478
msgid "Undefined location" msgid "Undefined location"
msgstr "" msgstr ""
@ -6717,7 +6742,7 @@ msgstr ""
msgid "Admin" msgid "Admin"
msgstr "" msgstr ""
#: templates/navbar.html:73 templates/registration/logout.html:5 #: templates/navbar.html:73
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
@ -6733,6 +6758,16 @@ msgstr ""
msgid "QR data not provided" msgid "QR data not provided"
msgstr "" msgstr ""
#: templates/registration/logged_out.html:50
msgid "You have been logged out"
msgstr ""
#: templates/registration/logged_out.html:51
#: templates/registration/password_reset_complete.html:51
#: templates/registration/password_reset_done.html:58
msgid "Return to login screen"
msgstr ""
#: templates/registration/login.html:64 #: templates/registration/login.html:64
msgid "Enter username" msgid "Enter username"
msgstr "" msgstr ""
@ -6745,16 +6780,52 @@ msgstr ""
msgid "Username / password combination is incorrect" msgid "Username / password combination is incorrect"
msgstr "" msgstr ""
#: templates/registration/logout.html:6 #: templates/registration/login.html:95
msgid "You have been logged out" #: templates/registration/password_reset_form.html:51
msgid "Forgotten your password?"
msgstr "" msgstr ""
#: templates/registration/logout.html:7 #: templates/registration/login.html:95
msgid "Click" msgid "Click here to reset"
msgstr "" msgstr ""
#: templates/registration/logout.html:7 #: templates/registration/password_reset_complete.html:50
msgid "here</a> to log in</p>" msgid "Password reset complete"
msgstr ""
#: templates/registration/password_reset_confirm.html:52
#: templates/registration/password_reset_confirm.html:56
msgid "Change password"
msgstr ""
#: templates/registration/password_reset_confirm.html:60
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a new password reset."
msgstr ""
#: templates/registration/password_reset_done.html:51
msgid ""
"We've emailed you instructions for setting your password, if an account "
"exists with the email you entered. You should receive them shortly."
msgstr ""
#: templates/registration/password_reset_done.html:54
msgid ""
"If you don't receive an email, please make sure you've entered the address "
"you registered with, and check your spam folder."
msgstr ""
#: templates/registration/password_reset_form.html:52
msgid "Enter your email address below."
msgstr ""
#: templates/registration/password_reset_form.html:53
msgid "An email will be sent with password reset instructions."
msgstr ""
#: templates/registration/password_reset_form.html:58
msgid "Send email"
msgstr "" msgstr ""
#: templates/stats.html:9 #: templates/stats.html:9
@ -6777,16 +6848,20 @@ msgstr ""
msgid "Issues detected" msgid "Issues detected"
msgstr "" msgstr ""
#: templates/stats.html:30 #: templates/stats.html:31
msgid "Background Worker" msgid "Background Worker"
msgstr "" msgstr ""
#: templates/stats.html:33 #: templates/stats.html:34
msgid "Operational" msgid "Background worker not running"
msgstr "" msgstr ""
#: templates/stats.html:35 #: templates/stats.html:42
msgid "Not running" msgid "Email Settings"
msgstr ""
#: templates/stats.html:45
msgid "Email settings not configured"
msgstr "" msgstr ""
#: templates/stock_table.html:14 #: templates/stock_table.html:14

View File

@ -181,6 +181,13 @@ $("#po-table").inventreeTable({
sortName: 'part__MPN', sortName: 'part__MPN',
field: 'supplier_part_detail.MPN', field: 'supplier_part_detail.MPN',
title: '{% trans "MPN" %}', title: '{% trans "MPN" %}',
formatter: function(value, row, index, field) {
if (row.supplier_part_detail.manufacturer_part) {
return renderLink(value, `/manufacturer-part/${row.supplier_part_detail.manufacturer_part.pk}/`);
} else {
return "";
}
},
}, },
{ {
sortable: true, sortable: true,

View File

@ -8,6 +8,7 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.http import JsonResponse from django.http import JsonResponse
from django.db.models import Q, F, Count, Prefetch, Sum from django.db.models import Q, F, Count, Prefetch, Sum
from django.utils.translation import ugettext_lazy as _
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
@ -36,7 +37,7 @@ from InvenTree.status_codes import BuildStatus
class PartCategoryTree(TreeSerializer): class PartCategoryTree(TreeSerializer):
title = "Parts" title = _("Parts")
model = PartCategory model = PartCategory
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()

View File

@ -16,7 +16,7 @@ from InvenTree.helpers import DownloadFile, GetExportFormats
from .admin import BomItemResource from .admin import BomItemResource
from .models import BomItem from .models import BomItem
from company.models import SupplierPart from company.models import ManufacturerPart, SupplierPart
def IsValidBOMFormat(fmt): def IsValidBOMFormat(fmt):
@ -49,7 +49,7 @@ def MakeBomTemplate(fmt):
return DownloadFile(data, filename) return DownloadFile(data, filename)
def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False): def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=False, stock_data=False, supplier_data=False, manufacturer_data=False):
""" Export a BOM (Bill of Materials) for a given part. """ Export a BOM (Bill of Materials) for a given part.
Args: Args:
@ -160,7 +160,123 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Add stock columns to dataset # Add stock columns to dataset
add_columns_to_dataset(stock_cols, len(bom_items)) add_columns_to_dataset(stock_cols, len(bom_items))
if supplier_data: if manufacturer_data and supplier_data:
"""
If requested, add extra columns for each SupplierPart and ManufacturerPart associated with each line item
"""
# Expand dataset with manufacturer parts
manufacturer_headers = [
_('Manufacturer'),
_('MPN'),
]
supplier_headers = [
_('Supplier'),
_('SKU'),
]
manufacturer_cols = {}
for b_idx, bom_item in enumerate(bom_items):
# Get part instance
b_part = bom_item.sub_part
# Filter manufacturer parts
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk)
manufacturer_parts = manufacturer_parts.prefetch_related('supplier_parts')
# Process manufacturer part
for manufacturer_idx, manufacturer_part in enumerate(manufacturer_parts):
if manufacturer_part:
manufacturer_name = manufacturer_part.manufacturer.name
else:
manufacturer_name = ''
manufacturer_mpn = manufacturer_part.MPN
# Generate column names for this manufacturer
k_man = manufacturer_headers[0] + "_" + str(manufacturer_idx)
k_mpn = manufacturer_headers[1] + "_" + str(manufacturer_idx)
try:
manufacturer_cols[k_man].update({b_idx: manufacturer_name})
manufacturer_cols[k_mpn].update({b_idx: manufacturer_mpn})
except KeyError:
manufacturer_cols[k_man] = {b_idx: manufacturer_name}
manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn}
# Process supplier parts
for supplier_idx, supplier_part in enumerate(manufacturer_part.supplier_parts.all()):
if supplier_part.supplier:
supplier_name = supplier_part.supplier.name
else:
supplier_name = ''
supplier_sku = supplier_part.SKU
# Generate column names for this supplier
k_sup = str(supplier_headers[0]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)
k_sku = str(supplier_headers[1]) + "_" + str(manufacturer_idx) + "_" + str(supplier_idx)
try:
manufacturer_cols[k_sup].update({b_idx: supplier_name})
manufacturer_cols[k_sku].update({b_idx: supplier_sku})
except KeyError:
manufacturer_cols[k_sup] = {b_idx: supplier_name}
manufacturer_cols[k_sku] = {b_idx: supplier_sku}
# Add manufacturer columns to dataset
add_columns_to_dataset(manufacturer_cols, len(bom_items))
elif manufacturer_data:
"""
If requested, add extra columns for each ManufacturerPart associated with each line item
"""
# Expand dataset with manufacturer parts
manufacturer_headers = [
_('Manufacturer'),
_('MPN'),
]
manufacturer_cols = {}
for b_idx, bom_item in enumerate(bom_items):
# Get part instance
b_part = bom_item.sub_part
# Filter supplier parts
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk)
for idx, manufacturer_part in enumerate(manufacturer_parts):
if manufacturer_part:
manufacturer_name = manufacturer_part.manufacturer.name
else:
manufacturer_name = ''
manufacturer_mpn = manufacturer_part.MPN
# Add manufacturer data to the manufacturer columns
# Generate column names for this manufacturer
k_man = manufacturer_headers[0] + "_" + str(idx)
k_mpn = manufacturer_headers[1] + "_" + str(idx)
try:
manufacturer_cols[k_man].update({b_idx: manufacturer_name})
manufacturer_cols[k_mpn].update({b_idx: manufacturer_mpn})
except KeyError:
manufacturer_cols[k_man] = {b_idx: manufacturer_name}
manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn}
# Add manufacturer columns to dataset
add_columns_to_dataset(manufacturer_cols, len(bom_items))
elif supplier_data:
""" """
If requested, add extra columns for each SupplierPart associated with each line item If requested, add extra columns for each SupplierPart associated with each line item
""" """
@ -169,8 +285,6 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
manufacturer_headers = [ manufacturer_headers = [
_('Supplier'), _('Supplier'),
_('SKU'), _('SKU'),
_('Manufacturer'),
_('MPN'),
] ]
manufacturer_cols = {} manufacturer_cols = {}
@ -191,31 +305,18 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
supplier_sku = supplier_part.SKU supplier_sku = supplier_part.SKU
if supplier_part.manufacturer:
manufacturer_name = supplier_part.manufacturer.name
else:
manufacturer_name = ''
manufacturer_mpn = supplier_part.MPN
# Add manufacturer data to the manufacturer columns # Add manufacturer data to the manufacturer columns
# Generate column names for this supplier # Generate column names for this supplier
k_sup = manufacturer_headers[0] + "_" + str(idx) k_sup = manufacturer_headers[0] + "_" + str(idx)
k_sku = manufacturer_headers[1] + "_" + str(idx) k_sku = manufacturer_headers[1] + "_" + str(idx)
k_man = manufacturer_headers[2] + "_" + str(idx)
k_mpn = manufacturer_headers[3] + "_" + str(idx)
try: try:
manufacturer_cols[k_sup].update({b_idx: supplier_name}) manufacturer_cols[k_sup].update({b_idx: supplier_name})
manufacturer_cols[k_sku].update({b_idx: supplier_sku}) manufacturer_cols[k_sku].update({b_idx: supplier_sku})
manufacturer_cols[k_man].update({b_idx: manufacturer_name})
manufacturer_cols[k_mpn].update({b_idx: manufacturer_mpn})
except KeyError: except KeyError:
manufacturer_cols[k_sup] = {b_idx: supplier_name} manufacturer_cols[k_sup] = {b_idx: supplier_name}
manufacturer_cols[k_sku] = {b_idx: supplier_sku} manufacturer_cols[k_sku] = {b_idx: supplier_sku}
manufacturer_cols[k_man] = {b_idx: manufacturer_name}
manufacturer_cols[k_mpn] = {b_idx: manufacturer_mpn}
# Add manufacturer columns to dataset # Add manufacturer columns to dataset
add_columns_to_dataset(manufacturer_cols, len(bom_items)) add_columns_to_dataset(manufacturer_cols, len(bom_items))

View File

@ -95,9 +95,11 @@ class BomExportForm(forms.Form):
parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM")) parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM"))
stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM")) stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM"))
manufacturer_data = forms.BooleanField(label=_("Include Manufacturer Data"), required=False, initial=True, help_text=_("Include part manufacturer data in exported BOM"))
supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM")) supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM"))
def get_choices(self): def get_choices(self):
""" BOM export format choices """ """ BOM export format choices """

View File

@ -0,0 +1,92 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% load inventree_extras %}
{% block menubar %}
{% include 'part/navbar.html' with tab='manufacturers' %}
{% endblock %}
{% block heading %}
{% trans "Part Manufacturers" %}
{% endblock %}
{% block details %}
<div id='button-toolbar'>
<div class='btn-group'>
<button class="btn btn-success" id='manufacturer-create'>
<span class='fas fa-plus-circle'></span> {% trans "New Manufacturer Part" %}
</button>
<div id='opt-dropdown' class="btn-group">
<button id='manufacturer-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href='#' id='manufacturer-part-delete' title='{% trans "Delete manufacturer parts" %}'>{% trans "Delete" %}</a></li>
</ul>
</div>
</div>
</div>
<table class="table table-striped table-condensed" id='manufacturer-table' data-toolbar='#button-toolbar'>
</table>
{% endblock %}
{% block js_load %}
{{ block.super }}
{% endblock %}
{% block js_ready %}
{{ block.super }}
$('#manufacturer-create').click(function () {
launchModalForm(
"{% url 'manufacturer-part-create' %}",
{
reload: true,
data: {
part: {{ part.id }}
},
secondary: [
{
field: 'manufacturer',
label: '{% trans "New Manufacturer" %}',
title: '{% trans "Create new manufacturer" %}',
url: "{% url 'manufacturer-create' %}",
}
]
});
});
$("#manufacturer-part-delete").click(function() {
var selections = $("#manufacturer-table").bootstrapTable("getSelections");
var parts = [];
selections.forEach(function(item) {
parts.push(item.pk);
});
launchModalForm("{% url 'manufacturer-part-delete' %}", {
data: {
parts: parts,
},
reload: true,
});
});
loadManufacturerPartTable(
"#manufacturer-table",
"{% url 'api-manufacturer-part-list' %}",
{
params: {
part: {{ part.id }},
part_detail: false,
manufacturer_detail: true,
},
}
);
linkButtonsToSelection($("#manufacturer-table"), ['#manufacturer-part-options'])
{% endblock %}

View File

@ -69,6 +69,12 @@
</li> </li>
{% endif %} {% endif %}
{% if part.purchaseable and roles.purchase_order.view %} {% if part.purchaseable and roles.purchase_order.view %}
<li class='list-group-item {% if tab == "manufacturers" %}active{% endif %}' title='{% trans "Manufacturers" %}'>
<a href='{% url "part-manufacturers" part.id %}'>
<span class='menu-tab-icon fas fa-industry'></span>
{% trans "Manufacturers" %}
</a>
</li>
<li class='list-group-item {% if tab == "suppliers" %}active{% endif %}' title='{% trans "Suppliers" %}'> <li class='list-group-item {% if tab == "suppliers" %}active{% endif %}' title='{% trans "Suppliers" %}'>
<a href='{% url "part-suppliers" part.id %}'> <a href='{% url "part-suppliers" part.id %}'>
<span class='menu-tab-icon fas fa-building'></span> <span class='menu-tab-icon fas fa-building'></span>

View File

@ -1,14 +1,15 @@
{% extends "modal_form.html" %} {% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %} {% block pre_form_content %}
<div class='alert alert-block alert-danger'> <div class='alert alert-block alert-danger'>
Are you sure you want to delete part '<b>{{ part.full_name }}</b>'? {% trans "Are you sure you want to delete part" %} '<b>{{ part.full_name }}</b>'?
</div> </div>
{% if part.used_in_count %} {% if part.used_in_count %}
<hr> <hr>
<p>This part is used in BOMs for {{ part.used_in_count }} other parts. If you delete this part, the BOMs for the following parts will be updated: <p>{% trans "This part is used in BOMs for" %} {{ part.used_in_count }} {% trans "other parts. If you delete this part, the BOMs for the following parts will be updated" %}:
<ul class="list-group"> <ul class="list-group">
{% for child in part.used_in.all %} {% for child in part.used_in.all %}
<li class='list-group-item'>{{ child.part.full_name }} - {{ child.part.description }}</li> <li class='list-group-item'>{{ child.part.full_name }} - {{ child.part.description }}</li>
@ -18,7 +19,7 @@
{% if part.stock_items.all|length > 0 %} {% if part.stock_items.all|length > 0 %}
<hr> <hr>
<p>There are {{ part.stock_items.all|length }} stock entries defined for this part. If you delete this part, the following stock entries will also be deleted: <p>{% trans "There are" %} {{ part.stock_items.all|length }} {% trans "stock entries defined for this part. If you delete this part, the following stock entries will also be deleted" %}:
<ul class='list-group'> <ul class='list-group'>
{% for stock in part.stock_items.all %} {% for stock in part.stock_items.all %}
<li class='list-group-item'>{{ stock }}</li> <li class='list-group-item'>{{ stock }}</li>
@ -27,9 +28,20 @@
</p> </p>
{% endif %} {% endif %}
{% if part.manufacturer_parts.all|length > 0 %}
<hr>
<p>{% trans "There are" %} {{ part.manufacturer_parts.all|length }} {% trans "manufacturers defined for this part. If you delete this part, the following manufacturer parts will also be deleted" %}:
<ul class='list-group'>
{% for spart in part.manufacturer_parts.all %}
<li class='list-group-item'>{{ spart.manufacturer.name }} - {{ spart.MPN }}</li>
{% endfor %}
</ul>
</p>
{% endif %}
{% if part.supplier_parts.all|length > 0 %} {% if part.supplier_parts.all|length > 0 %}
<hr> <hr>
<p>There are {{ part.supplier_parts.all|length }} suppliers defined for this part. If you delete this part, the following supplier parts will also be deleted. <p>{% trans "There are" %} {{ part.supplier_parts.all|length }} {% trans "suppliers defined for this part. If you delete this part, the following supplier parts will also be deleted" %}:
<ul class='list-group'> <ul class='list-group'>
{% for spart in part.supplier_parts.all %} {% for spart in part.supplier_parts.all %}
<li class='list-group-item'>{{ spart.supplier.name }} - {{ spart.SKU }}</li> <li class='list-group-item'>{{ spart.supplier.name }} - {{ spart.SKU }}</li>
@ -40,7 +52,7 @@
{% if part.serials.all|length > 0 %} {% if part.serials.all|length > 0 %}
<hr> <hr>
<p>There are {{ part.serials.all|length }} unique parts tracked for '{{ part.full_name }}'. Deleting this part will permanently remove this tracking information.</p> <p>{% trans "There are" %} {{ part.serials.all|length }} {% trans "unique parts tracked for" %} '{{ part.full_name }}'. {% trans "Deleting this part will permanently remove this tracking information" %}.</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -85,7 +85,7 @@
{ {
params: { params: {
part: {{ part.id }}, part: {{ part.id }},
part_detail: true, part_detail: false,
supplier_detail: true, supplier_detail: true,
manufacturer_detail: true, manufacturer_detail: true,
}, },

View File

@ -60,6 +60,7 @@ part_detail_urls = [
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
url(r'^manufacturers/?', views.PartDetail.as_view(template_name='part/manufacturer.html'), name='part-manufacturers'),
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'), url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'),
url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'), url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'),

View File

@ -1845,6 +1845,8 @@ class BomDownload(AjaxView):
supplier_data = str2bool(request.GET.get('supplier_data', False)) supplier_data = str2bool(request.GET.get('supplier_data', False))
manufacturer_data = str2bool(request.GET.get('manufacturer_data', False))
levels = request.GET.get('levels', None) levels = request.GET.get('levels', None)
if levels is not None: if levels is not None:
@ -1866,7 +1868,9 @@ class BomDownload(AjaxView):
max_levels=levels, max_levels=levels,
parameter_data=parameter_data, parameter_data=parameter_data,
stock_data=stock_data, stock_data=stock_data,
supplier_data=supplier_data) supplier_data=supplier_data,
manufacturer_data=manufacturer_data,
)
def get_data(self): def get_data(self):
return { return {
@ -1896,6 +1900,7 @@ class BomExport(AjaxView):
parameter_data = str2bool(request.POST.get('parameter_data', False)) parameter_data = str2bool(request.POST.get('parameter_data', False))
stock_data = str2bool(request.POST.get('stock_data', False)) stock_data = str2bool(request.POST.get('stock_data', False))
supplier_data = str2bool(request.POST.get('supplier_data', False)) supplier_data = str2bool(request.POST.get('supplier_data', False))
manufacturer_data = str2bool(request.POST.get('manufacturer_data', False))
try: try:
part = Part.objects.get(pk=self.kwargs['pk']) part = Part.objects.get(pk=self.kwargs['pk'])
@ -1913,6 +1918,7 @@ class BomExport(AjaxView):
url += '&parameter_data=' + str(parameter_data) url += '&parameter_data=' + str(parameter_data)
url += '&stock_data=' + str(stock_data) url += '&stock_data=' + str(stock_data)
url += '&supplier_data=' + str(supplier_data) url += '&supplier_data=' + str(supplier_data)
url += '&manufacturer_data=' + str(manufacturer_data)
if levels: if levels:
url += '&levels=' + str(levels) url += '&levels=' + str(levels)

View File

@ -48,7 +48,7 @@ from rest_framework import generics, filters, permissions
class StockCategoryTree(TreeSerializer): class StockCategoryTree(TreeSerializer):
title = 'Stock' title = _('Stock')
model = StockLocation model = StockLocation
@property @property
@ -774,7 +774,7 @@ class StockList(generics.ListCreateAPIView):
company = params.get('company', None) company = params.get('company', None)
if company is not None: if company is not None:
queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer=company)) queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company))
# Filter by supplier # Filter by supplier
supplier = params.get('supplier', None) supplier = params.get('supplier', None)
@ -786,7 +786,7 @@ class StockList(generics.ListCreateAPIView):
manufacturer = params.get('manufacturer', None) manufacturer = params.get('manufacturer', None)
if manufacturer is not None: if manufacturer is not None:
queryset = queryset.filter(supplier_part__manufacturer=manufacturer) queryset = queryset.filter(supplier_part__manufacturer_part__manufacturer=manufacturer)
""" """
Filter by the 'last updated' date of the stock item(s): Filter by the 'last updated' date of the stock item(s):

View File

@ -344,6 +344,8 @@ class StockItem(MPTTModel):
"stockitem", "stockitem",
self.id, self.id,
{ {
"request": kwargs.get('request', None),
"item_url": reverse('stock-item-detail', kwargs={'pk': self.id}),
"url": reverse('api-stock-detail', kwargs={'pk': self.id}), "url": reverse('api-stock-detail', kwargs={'pk': self.id}),
}, },
**kwargs **kwargs

View File

@ -84,7 +84,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
'sales_order', 'sales_order',
'supplier_part', 'supplier_part',
'supplier_part__supplier', 'supplier_part__supplier',
'supplier_part__manufacturer', 'supplier_part__manufacturer_part__manufacturer',
'allocations', 'allocations',
'sales_order_allocations', 'sales_order_allocations',
'location', 'location',

View File

@ -331,9 +331,21 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<td><a href="{{ item.link }}">{{ item.link }}</a></td> <td><a href="{{ item.link }}">{{ item.link }}</a></td>
</tr> </tr>
{% endif %} {% endif %}
{% if item.supplier_part %} {% if item.supplier_part.manufacturer_part %}
<tr> <tr>
<td><span class='fas fa-industry'></span></td> <td><span class='fas fa-industry'></span></td>
<td>{% trans "Manufacturer" %}</td>
<td><a href="{% url 'company-detail' item.supplier_part.manufacturer_part.manufacturer.id %}">{{ item.supplier_part.manufacturer_part.manufacturer.name }}</a></td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Manufacturer Part" %}</td>
<td><a href="{% url 'manufacturer-part-detail' item.supplier_part.manufacturer_part.id %}">{{ item.supplier_part.manufacturer_part.MPN }}</a></td>
</tr>
{% endif %}
{% if item.supplier_part %}
<tr>
<td><span class='fas fa-building'></span></td>
<td>{% trans "Supplier" %}</td> <td>{% trans "Supplier" %}</td>
<td><a href="{% url 'company-detail' item.supplier_part.supplier.id %}">{{ item.supplier_part.supplier.name }}</a></td> <td><a href="{% url 'company-detail' item.supplier_part.supplier.id %}">{{ item.supplier_part.supplier.name }}</a></td>
</tr> </tr>

View File

@ -2,6 +2,7 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load inventree_extras %}
{% block page_title %} {% block page_title %}
InvenTree | {% trans "Search Results" %} InvenTree | {% trans "Search Results" %}
@ -145,6 +146,21 @@ InvenTree | {% trans "Search Results" %}
], ],
}); });
addItem('manufacturer-part', '{% trans "Manufacturer Parts" %}', 'fa-toolbox');
loadManufacturerPartTable(
"#table-manufacturer-part",
"{% url 'api-manufacturer-part-list' %}",
{
params: {
search: "{{ query }}",
part_detail: true,
supplier_detail: true,
manufacturer_detail: true
},
}
);
addItem('supplier-part', '{% trans "Supplier Parts" %}', 'fa-pallet'); addItem('supplier-part', '{% trans "Supplier Parts" %}', 'fa-pallet');
loadSupplierPartTable( loadSupplierPartTable(
@ -287,6 +303,15 @@ InvenTree | {% trans "Search Results" %}
{% if roles.purchase_order.view or roles.sales_order.view %} {% if roles.purchase_order.view or roles.sales_order.view %}
addItemTitle('{% trans "Company" %}'); addItemTitle('{% trans "Company" %}');
addItem('manufacturer', '{% trans "Manufacturers" %}', 'fa-industry');
loadCompanyTable('#table-manufacturer', "{% url 'api-company-list' %}", {
params: {
search: "{{ query }}",
is_manufacturer: "true",
}
});
{% if roles.purchase_order.view %} {% if roles.purchase_order.view %}
addItem('supplier', '{% trans "Suppliers" %}', 'fa-building'); addItem('supplier', '{% trans "Suppliers" %}', 'fa-building');
@ -305,16 +330,6 @@ InvenTree | {% trans "Search Results" %}
} }
}); });
addItem('manufacturer', '{% trans "Manufacturers" %}', 'fa-industry');
loadCompanyTable('#table-manufacturer', "{% url 'api-company-list' %}", {
params: {
search: "{{ query }}",
is_manufacturer: "true",
}
});
{% endif %} {% endif %}
{% if roles.sales_order.view %} {% if roles.sales_order.view %}

View File

@ -101,6 +101,104 @@ function loadCompanyTable(table, url, options={}) {
} }
function loadManufacturerPartTable(table, url, options) {
/*
* Load manufacturer part table
*
*/
// Query parameters
var params = options.params || {};
// Load filters
var filters = loadTableFilters("manufacturer-part");
for (var key in params) {
filters[key] = params[key];
}
setupFilterList("manufacturer-part", $(table));
$(table).inventreeTable({
url: url,
method: 'get',
original: params,
queryParams: filters,
name: 'manufacturerparts',
groupBy: false,
formatNoMatches: function() { return "{% trans "No manufacturer parts found" %}"; },
columns: [
{
checkbox: true,
switchable: false,
},
{
visible: params['part_detail'],
switchable: params['part_detail'],
sortable: true,
field: 'part_detail.full_name',
title: '{% trans "Part" %}',
formatter: function(value, row, index, field) {
var url = `/part/${row.part}/`;
var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url);
if (row.part_detail.is_template) {
html += `<span class='fas fa-clone label-right' title='{% trans "Template part" %}'></span>`;
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools label-right' title='{% trans "Assembled part" %}'></span>`;
}
if (!row.part_detail.active) {
html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`;
}
return html;
}
},
{
sortable: true,
field: 'manufacturer',
title: '{% trans "Manufacturer" %}',
formatter: function(value, row, index, field) {
if (value && row.manufacturer_detail) {
var name = row.manufacturer_detail.name;
var url = `/company/${value}/`;
var html = imageHoverIcon(row.manufacturer_detail.image) + renderLink(name, url);
return html;
} else {
return "-";
}
}
},
{
sortable: true,
field: 'MPN',
title: '{% trans "MPN" %}',
formatter: function(value, row, index, field) {
return renderLink(value, `/manufacturer-part/${row.pk}/`);
}
},
{
field: 'link',
title: '{% trans "Link" %}',
formatter: function(value, row, index, field) {
if (value) {
return renderLink(value, value);
} else {
return '';
}
}
},
],
});
}
function loadSupplierPartTable(table, url, options) { function loadSupplierPartTable(table, url, options) {
/* /*
* Load supplier part table * Load supplier part table
@ -133,10 +231,11 @@ function loadSupplierPartTable(table, url, options) {
switchable: false, switchable: false,
}, },
{ {
visible: params['part_detail'],
switchable: params['part_detail'],
sortable: true, sortable: true,
field: 'part_detail.full_name', field: 'part_detail.full_name',
title: '{% trans "Part" %}', title: '{% trans "Part" %}',
switchable: false,
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var url = `/part/${row.part}/`; var url = `/part/${row.part}/`;
@ -183,6 +282,8 @@ function loadSupplierPartTable(table, url, options) {
} }
}, },
{ {
visible: params['manufacturer_detail'],
switchable: params['manufacturer_detail'],
sortable: true, sortable: true,
field: 'manufacturer', field: 'manufacturer',
title: '{% trans "Manufacturer" %}', title: '{% trans "Manufacturer" %}',
@ -199,9 +300,18 @@ function loadSupplierPartTable(table, url, options) {
} }
}, },
{ {
visible: params['manufacturer_detail'],
switchable: params['manufacturer_detail'],
sortable: true, sortable: true,
field: 'MPN', field: 'MPN',
title: '{% trans "MPN" %}', title: '{% trans "MPN" %}',
formatter: function(value, row, index, field) {
if (value && row.manufacturer_part) {
return renderLink(value, `/manufacturer-part/${row.manufacturer_part.pk}/`);
} else {
return "-";
}
}
}, },
{ {
field: 'link', field: 'link',

View File

@ -164,11 +164,11 @@ function getFilterOptionList(tableKey, filterKey) {
return { return {
'1': { '1': {
key: '1', key: '1',
value: 'true', value: '{% trans "true" %}',
}, },
'0': { '0': {
key: '0', key: '0',
value: 'false', value: '{% trans "false" %}',
}, },
}; };
} else if ('options' in settings) { } else if ('options' in settings) {
@ -394,8 +394,8 @@ function getFilterOptionValue(tableKey, filterKey, valueKey) {
// Lookup for boolean options // Lookup for boolean options
if (filter.type == 'bool') { if (filter.type == 'bool') {
if (value == '1') return 'true'; if (value == '1') return '{% trans "true" %}';
if (value == '0') return 'false'; if (value == '0') return '{% trans "false" %}';
return value; return value;
} }

View File

@ -354,7 +354,7 @@ function loadStockTable(table, options) {
var html = imageHoverIcon(row.part_detail.thumbnail); var html = imageHoverIcon(row.part_detail.thumbnail);
html += row.part_detail.full_name; html += row.part_detail.full_name;
html += ` <i>(${data.length} items)</i>`; html += ` <i>(${data.length} {% trans "items" %})</i>`;
html += makePartIcons(row.part_detail); html += makePartIcons(row.part_detail);
@ -446,7 +446,7 @@ function loadStockTable(table, options) {
}); });
if (batches.length > 1) { if (batches.length > 1) {
return "" + batches.length + " batches"; return "" + batches.length + " {% trans 'batches' %}";
} else if (batches.length == 1) { } else if (batches.length == 1) {
if (batches[0]) { if (batches[0]) {
return batches[0]; return batches[0];
@ -473,9 +473,9 @@ function loadStockTable(table, options) {
// Single location, easy! // Single location, easy!
return locations[0]; return locations[0];
} else if (locations.length > 1) { } else if (locations.length > 1) {
return "In " + locations.length + " locations"; return "In " + locations.length + " {% trans 'locations' %}";
} else { } else {
return "<i>{% trans "Undefined location" %}</i>"; return "<i>{% trans 'Undefined location' %}</i>";
} }
} else if (field == 'notes') { } else if (field == 'notes') {
var notes = []; var notes = [];
@ -1219,7 +1219,7 @@ function loadInstalledInTable(table, options) {
// Add some buttons yo! // Add some buttons yo!
html += `<div class='btn-group float-right' role='group'>`; html += `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans "Uninstall stock item" %}"); html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans 'Uninstall stock item' %}");
html += `</div>`; html += `</div>`;

View File

@ -59,11 +59,17 @@
{% endif %} {% endif %}
<li class='dropdown'> <li class='dropdown'>
<a class='dropdown-toggle' data-toggle='dropdown' href="#"> <a class='dropdown-toggle' data-toggle='dropdown' href="#">
{% if user.is_staff %}
{% if not system_healthy %} {% if not system_healthy %}
<span class='fas fa-exclamation-triangle icon-red'></span> {% if not django_q_running %}
<span class='fas fa-exclamation-triangle icon-red'></span>
{% else %}
<span class='fas fa-exclamation-triangle icon-orange'></span>
{% endif %}
{% elif not up_to_date %} {% elif not up_to_date %}
<span class='fas fa-info-circle icon-green'></span> <span class='fas fa-info-circle icon-green'></span>
{% endif %} {% endif %}
{% endif %}
<span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a> <span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a>
<ul class='dropdown-menu'> <ul class='dropdown-menu'>
{% if user.is_authenticated %} {% if user.is_authenticated %}
@ -77,10 +83,14 @@
<hr> <hr>
<li><a href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li> <li><a href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
<li id='launch-stats'><a href='#'> <li id='launch-stats'><a href='#'>
{% if system_healthy %} {% if system_healthy or not user.is_staff %}
<span class='fas fa-server'> <span class='fas fa-server'></span>
{% else %} {% else %}
<span class='fas fa-server icon-red'> {% if not django_q_running %}
<span class='fas fa-server icon-red'></span>
{% else %}
<span class='fas fa-server icon-orange'></span>
{% endif %}
{% endif %} {% endif %}
</span> {% trans "System Information" %} </span> {% trans "System Information" %}
</a></li> </a></li>

View File

@ -74,6 +74,7 @@ class RuleSet(models.Model):
'part_partrelated', 'part_partrelated',
'part_partstar', 'part_partstar',
'company_supplierpart', 'company_supplierpart',
'company_manufacturerpart',
], ],
'stock_location': [ 'stock_location': [
'stock_stocklocation', 'stock_stocklocation',