diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index c2441590f5..6f6953ccb5 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -280,11 +280,25 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs):
json string of the supplied data plus some other data
"""
+ url = kwargs.get('url', False)
brief = kwargs.get('brief', True)
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
else:
data['tool'] = 'InvenTree'
diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css
index 153931f974..9d322f339d 100644
--- a/InvenTree/InvenTree/static/css/inventree.css
+++ b/InvenTree/InvenTree/static/css/inventree.css
@@ -185,6 +185,10 @@
color: #c55;
}
+.icon-orange {
+ color: #fcba03;
+}
+
.icon-green {
color: #43bb43;
}
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 88160e76c1..da7799397e 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -11,6 +11,7 @@ from django.contrib import admin
from django.contrib.auth import views as auth_views
from company.urls import company_urls
+from company.urls import manufacturer_part_urls
from company.urls import supplier_part_urls
from company.urls import price_break_urls
@@ -115,6 +116,7 @@ dynamic_javascript_urls = [
urlpatterns = [
url(r'^part/', include(part_urls)),
+ url(r'^manufacturer-part/', include(manufacturer_part_urls)),
url(r'^supplier-part/', include(supplier_part_urls)),
url(r'^price-break/', include(price_break_urls)),
diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py
index ebf2924d16..66557b783b 100644
--- a/InvenTree/company/api.py
+++ b/InvenTree/company/api.py
@@ -15,9 +15,11 @@ from django.db.models import Q
from InvenTree.helpers import str2bool
from .models import Company
+from .models import ManufacturerPart
from .models import SupplierPart, SupplierPriceBreak
from .serializers import CompanySerializer
+from .serializers import ManufacturerPartSerializer
from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer
@@ -80,8 +82,105 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = CompanySerializer.annotate_queryset(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):
""" API endpoint for list view of SupplierPart object
@@ -92,7 +191,7 @@ class SupplierPartList(generics.ListCreateAPIView):
queryset = SupplierPart.objects.all().prefetch_related(
'part',
'supplier',
- 'manufacturer'
+ 'manufacturer_part__manufacturer',
)
def get_queryset(self):
@@ -114,7 +213,7 @@ class SupplierPartList(generics.ListCreateAPIView):
manufacturer = params.get('manufacturer', None)
if manufacturer is not None:
- queryset = queryset.filter(manufacturer=manufacturer)
+ queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer)
# Filter by supplier
supplier = params.get('supplier', None)
@@ -126,7 +225,7 @@ class SupplierPartList(generics.ListCreateAPIView):
company = params.get('company', 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?
part = params.get('part', None)
@@ -134,6 +233,12 @@ class SupplierPartList(generics.ListCreateAPIView):
if part is not None:
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?
active = params.get('active', None)
@@ -184,9 +289,9 @@ class SupplierPartList(generics.ListCreateAPIView):
search_fields = [
'SKU',
'supplier__name',
- 'manufacturer__name',
+ 'manufacturer_part__manufacturer__name',
'description',
- 'MPN',
+ 'manufacturer_part__MPN',
'part__name',
'part__description',
]
@@ -197,7 +302,7 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
- GET: Retrieve detail view
- PATCH: Update object
- - DELETE: Delete objec
+ - DELETE: Delete object
"""
queryset = SupplierPart.objects.all()
@@ -226,6 +331,15 @@ class SupplierPriceBreakList(generics.ListCreateAPIView):
]
+manufacturer_part_api_urls = [
+
+ url(r'^(?P\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 = [
url(r'^(?P\d+)/?', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
@@ -236,7 +350,8 @@ supplier_part_api_urls = [
company_api_urls = [
-
+ url(r'^part/manufacturer/', include(manufacturer_part_api_urls)),
+
url(r'^part/', include(supplier_part_api_urls)),
url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'),
diff --git a/InvenTree/company/fixtures/company.yaml b/InvenTree/company/fixtures/company.yaml
index 8301eb0f5e..c302b6efad 100644
--- a/InvenTree/company/fixtures/company.yaml
+++ b/InvenTree/company/fixtures/company.yaml
@@ -31,3 +31,17 @@
name: Another customer!
description: Yet another company
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
diff --git a/InvenTree/company/fixtures/manufacturer_part.yaml b/InvenTree/company/fixtures/manufacturer_part.yaml
new file mode 100644
index 0000000000..880a0e5862
--- /dev/null
+++ b/InvenTree/company/fixtures/manufacturer_part.yaml
@@ -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'
diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py
index 2677402334..8ad8c6bfea 100644
--- a/InvenTree/company/forms.py
+++ b/InvenTree/company/forms.py
@@ -17,6 +17,7 @@ from djmoney.forms.fields import MoneyField
import common.settings
from .models import Company
+from .models import ManufacturerPart
from .models import SupplierPart
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):
""" Form for editing a SupplierPart object """
field_prefix = {
'link': 'fa-link',
- 'MPN': 'fa-hashtag',
'SKU': 'fa-hashtag',
'note': 'fa-pencil-alt',
}
@@ -104,15 +123,28 @@ class EditSupplierPartForm(HelperForm):
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:
model = SupplierPart
fields = [
'part',
'supplier',
'SKU',
- 'description',
'manufacturer',
'MPN',
+ 'description',
'link',
'note',
'single_pricing',
@@ -121,6 +153,19 @@ class EditSupplierPartForm(HelperForm):
'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):
""" Form for creating / editing a supplier price break """
diff --git a/InvenTree/company/migrations/0034_manufacturerpart.py b/InvenTree/company/migrations/0034_manufacturerpart.py
new file mode 100644
index 0000000000..2e8a8bf82f
--- /dev/null
+++ b/InvenTree/company/migrations/0034_manufacturerpart.py
@@ -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')},
+ },
+ ),
+ ]
diff --git a/InvenTree/company/migrations/0035_supplierpart_update_1.py b/InvenTree/company/migrations/0035_supplierpart_update_1.py
new file mode 100644
index 0000000000..657ae2464c
--- /dev/null
+++ b/InvenTree/company/migrations/0035_supplierpart_update_1.py
@@ -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'),
+ ),
+ ]
diff --git a/InvenTree/company/migrations/0036_supplierpart_update_2.py b/InvenTree/company/migrations/0036_supplierpart_update_2.py
new file mode 100644
index 0000000000..52a470be92
--- /dev/null
+++ b/InvenTree/company/migrations/0036_supplierpart_update_2.py
@@ -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),
+ ]
diff --git a/InvenTree/company/migrations/0037_supplierpart_update_3.py b/InvenTree/company/migrations/0037_supplierpart_update_3.py
new file mode 100644
index 0000000000..e3384be513
--- /dev/null
+++ b/InvenTree/company/migrations/0037_supplierpart_update_3.py
@@ -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',
+ ),
+ ]
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index d83748c930..3ea50b1622 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -11,7 +11,9 @@ import math
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator
+from django.core.exceptions import ValidationError
from django.db import models
+from django.db.utils import IntegrityError
from django.db.models import Sum, Q, UniqueConstraint
from django.apps import apps
@@ -208,7 +210,7 @@ class Company(models.Model):
@property
def parts(self):
""" 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
def part_count(self):
@@ -223,7 +225,7 @@ class Company(models.Model):
def stock_items(self):
""" Return a list of all stock items supplied or manufactured by this company """
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
def stock_count(self):
@@ -284,19 +286,106 @@ class Contact(models.Model):
on_delete=models.CASCADE)
-class SupplierPart(models.Model):
- """ Represents a unique part as provided by a Supplier
- Each SupplierPart is identified by a MPN (Manufacturer Part Number)
- Each SupplierPart is also linked to a Part object.
- A Part may be available from multiple suppliers
+class ManufacturerPart(models.Model):
+ """ Represents a unique part as provided by a Manufacturer
+ Each ManufacturerPart is identified by a MPN (Manufacturer Part Number)
+ Each ManufacturerPart is also linked to a Part object.
+ A Part may be available from multiple manufacturers
Attributes:
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
SKU: Stock keeping unit (supplier part number)
- manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!)
- MPN: Manufacture part number
- link: Link to external website for this part
+ link: Link to external website for this supplier part
description: Descriptive notes field
note: Longer form note field
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):
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:
unique_together = ('part', 'supplier', 'SKU')
@@ -336,23 +476,12 @@ class SupplierPart(models.Model):
help_text=_('Supplier stock keeping unit')
)
- manufacturer = models.ForeignKey(
- Company,
- on_delete=models.SET_NULL,
- related_name='manufactured_parts',
- limit_choices_to={
- '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')
- )
+ manufacturer_part = models.ForeignKey(ManufacturerPart, on_delete=models.CASCADE,
+ blank=True, null=True,
+ related_name='supplier_parts',
+ verbose_name=_('Manufacturer Part'),
+ help_text=_('Select manufacturer part'),
+ )
link = InvenTreeURLField(
blank=True, null=True,
@@ -389,10 +518,11 @@ class SupplierPart(models.Model):
items = []
- if self.manufacturer:
- items.append(self.manufacturer.name)
- if self.MPN:
- items.append(self.MPN)
+ if self.manufacturer_part:
+ if self.manufacturer_part.manufacturer:
+ items.append(self.manufacturer_part.manufacturer.name)
+ if self.manufacturer_part.MPN:
+ items.append(self.manufacturer_part.MPN)
return ' | '.join(items)
diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py
index 4951bd3ad0..35e84aac1e 100644
--- a/InvenTree/company/serializers.py
+++ b/InvenTree/company/serializers.py
@@ -7,6 +7,7 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount
from .models import Company
+from .models import ManufacturerPart
from .models import SupplierPart, SupplierPriceBreak
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):
""" Serializer for SupplierPart object """
@@ -87,7 +131,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
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)
@@ -113,8 +157,12 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
self.fields.pop('pretty_name')
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:
model = SupplierPart
@@ -127,12 +175,31 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
'supplier_detail',
'SKU',
'manufacturer',
- 'manufacturer_detail',
- 'description',
'MPN',
+ 'manufacturer_detail',
+ 'manufacturer_part',
+ 'description',
'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):
""" Serializer for SupplierPriceBreak object """
diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html
index b803fc5852..8e41b2d639 100644
--- a/InvenTree/company/templates/company/detail.html
+++ b/InvenTree/company/templates/company/detail.html
@@ -21,11 +21,13 @@
{% trans "Company Name" %}
{{ company.name }}
+ {% if company.description %}
{% trans "Description" %}
{{ company.description }}
+ {% endif %}
{% trans "Website" %}
diff --git a/InvenTree/company/templates/company/detail_manufacturer_part.html b/InvenTree/company/templates/company/detail_manufacturer_part.html
new file mode 100644
index 0000000000..902d456eaf
--- /dev/null
+++ b/InvenTree/company/templates/company/detail_manufacturer_part.html
@@ -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 %}
+
+{% endif %}
+
+
+{% 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 %}
\ No newline at end of file
diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_supplier_part.html
similarity index 80%
rename from InvenTree/company/templates/company/detail_part.html
rename to InvenTree/company/templates/company/detail_supplier_part.html
index 05aaf78955..bf92f96843 100644
--- a/InvenTree/company/templates/company/detail_part.html
+++ b/InvenTree/company/templates/company/detail_supplier_part.html
@@ -1,9 +1,10 @@
{% extends "company/company_base.html" %}
{% load static %}
{% load i18n %}
+{% load inventree_extras %}
{% block menubar %}
-{% include 'company/navbar.html' with tab='parts' %}
+{% include 'company/navbar.html' with tab='supplier_parts' %}
{% endblock %}
{% block heading %}
@@ -17,9 +18,9 @@
{% trans "SKU" %}
{{ part.SKU }}
- {% if part.manufacturer %}
+ {% if part.manufacturer_part.manufacturer %}
{% trans "Manufacturer" %}
- {{ part.manufacturer.name }}
+ {{ part.manufacturer_part.manufacturer.name }}
+
{% endif %}
- {% if part.MPN %}
+ {% if part.manufacturer_part.MPN %}
{% trans "MPN" %}
- {{ part.MPN }}
+ {{ part.manufacturer_part.MPN }}
{% endif %}
{% if part.packaging %}
@@ -150,7 +151,7 @@ $('#delete-part').click(function() {
launchModalForm(
"{% url 'supplier-part-delete' %}?part={{ part.id }}",
{
- redirect: "{% url 'company-detail-parts' part.supplier.id %}"
+ redirect: "{% url 'company-detail-supplier-parts' part.supplier.id %}"
}
);
});
diff --git a/InvenTree/company/templates/company/partdelete.html b/InvenTree/company/templates/company/supplier_part_delete.html
similarity index 92%
rename from InvenTree/company/templates/company/partdelete.html
rename to InvenTree/company/templates/company/supplier_part_delete.html
index 8eed487697..b44e7c0610 100644
--- a/InvenTree/company/templates/company/partdelete.html
+++ b/InvenTree/company/templates/company/supplier_part_delete.html
@@ -13,13 +13,16 @@
+
+ {% include "hover_image.html" with image=part.part.image %}
+ {{ part.part.full_name }}
+
{% include "hover_image.html" with image=part.supplier.image %}
{{ part.supplier.name }}
- {% include "hover_image.html" with image=part.part.image %}
- {{ part.part.full_name }}
+ {{ part.SKU }}
{% endfor %}
diff --git a/InvenTree/company/templates/company/supplier_part_detail.html b/InvenTree/company/templates/company/supplier_part_detail.html
index d3b94f0ad5..2938909757 100644
--- a/InvenTree/company/templates/company/supplier_part_detail.html
+++ b/InvenTree/company/templates/company/supplier_part_detail.html
@@ -3,7 +3,7 @@
{% load i18n %}
{% block menubar %}
-{% include "company/part_navbar.html" with tab='details' %}
+{% include "company/supplier_part_navbar.html" with tab='details' %}
{% endblock %}
{% block heading %}
@@ -22,7 +22,7 @@
{% endif %}
- {% trans "Supplier" %} {{ part.supplier.name }}
+ {% trans "Supplier" %} {{ part.supplier.name }}
{% trans "SKU" %} {{ part.SKU }}
{% if part.link %}
{% trans "External Link" %} {{ part.link }}
diff --git a/InvenTree/company/templates/company/part_navbar.html b/InvenTree/company/templates/company/supplier_part_navbar.html
similarity index 97%
rename from InvenTree/company/templates/company/part_navbar.html
rename to InvenTree/company/templates/company/supplier_part_navbar.html
index 4236e7b734..09c3bc0443 100644
--- a/InvenTree/company/templates/company/part_navbar.html
+++ b/InvenTree/company/templates/company/supplier_part_navbar.html
@@ -1,4 +1,5 @@
{% load i18n %}
+{% load inventree_extras %}
diff --git a/InvenTree/company/templates/company/supplier_part_orders.html b/InvenTree/company/templates/company/supplier_part_orders.html
index f01bfa68d1..d523bea894 100644
--- a/InvenTree/company/templates/company/supplier_part_orders.html
+++ b/InvenTree/company/templates/company/supplier_part_orders.html
@@ -3,7 +3,7 @@
{% load i18n %}
{% block menubar %}
-{% include "company/part_navbar.html" with tab='orders' %}
+{% include "company/supplier_part_navbar.html" with tab='orders' %}
{% endblock %}
{% block heading %}
diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html
index 2314b5cfcf..a674837650 100644
--- a/InvenTree/company/templates/company/supplier_part_pricing.html
+++ b/InvenTree/company/templates/company/supplier_part_pricing.html
@@ -4,7 +4,7 @@
{% load inventree_extras %}
{% block menubar %}
-{% include "company/part_navbar.html" with tab='pricing' %}
+{% include "company/supplier_part_navbar.html" with tab='pricing' %}
{% endblock %}
{% block heading %}
diff --git a/InvenTree/company/templates/company/supplier_part_stock.html b/InvenTree/company/templates/company/supplier_part_stock.html
index 524c508957..1187b95bca 100644
--- a/InvenTree/company/templates/company/supplier_part_stock.html
+++ b/InvenTree/company/templates/company/supplier_part_stock.html
@@ -3,7 +3,7 @@
{% load i18n %}
{% block menubar %}
-{% include "company/part_navbar.html" with tab='stock' %}
+{% include "company/supplier_part_navbar.html" with tab='stock' %}
{% endblock %}
{% block heading %}
diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py
index 219a8ac019..a65beb4dc2 100644
--- a/InvenTree/company/test_api.py
+++ b/InvenTree/company/test_api.py
@@ -27,7 +27,7 @@ class CompanyTest(InvenTreeAPITestCase):
def test_company_list(self):
url = reverse('api-company-list')
- # There should be two companies
+ # There should be three companies
response = self.get(url)
self.assertEqual(len(response.data), 3)
@@ -62,3 +62,90 @@ class CompanyTest(InvenTreeAPITestCase):
data = {'search': 'cup'}
response = self.get(url, data)
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')
diff --git a/InvenTree/company/test_migrations.py b/InvenTree/company/test_migrations.py
index 1a51a5a5b0..bf6e212f7a 100644
--- a/InvenTree/company/test_migrations.py
+++ b/InvenTree/company/test_migrations.py
@@ -79,7 +79,7 @@ class TestManufacturerField(MigratorTestCase):
part=part,
supplier=supplier,
SKU='SCREW.002',
- manufacturer_name='Zero Corp'
+ manufacturer_name='Zero Corp',
)
self.assertEqual(Company.objects.count(), 1)
@@ -107,6 +107,136 @@ class TestManufacturerField(MigratorTestCase):
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):
"""
Tests for upgrade from basic currency support to django-money
diff --git a/InvenTree/company/test_views.py b/InvenTree/company/test_views.py
index 0163e65c29..e6eb54e0bf 100644
--- a/InvenTree/company/test_views.py
+++ b/InvenTree/company/test_views.py
@@ -10,6 +10,7 @@ from django.urls import reverse
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
+from .models import ManufacturerPart
from .models import SupplierPart
@@ -20,6 +21,7 @@ class CompanyViewTestBase(TestCase):
'part',
'location',
'company',
+ 'manufacturer_part',
'supplier_part',
]
@@ -200,3 +202,105 @@ class CompanyViewTest(CompanyViewTestBase):
response = self.client.get(reverse('customer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
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())
diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py
index f7bcd4e0b6..cb366e8a6d 100644
--- a/InvenTree/company/tests.py
+++ b/InvenTree/company/tests.py
@@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
import os
-from .models import Company, Contact, SupplierPart
+from .models import Company, Contact, ManufacturerPart, SupplierPart
from .models import rename_company_image
from part.models import Part
@@ -22,6 +22,7 @@ class CompanySimpleTest(TestCase):
'part',
'location',
'bom',
+ 'manufacturer_part',
'supplier_part',
'price_breaks',
]
@@ -74,10 +75,10 @@ class CompanySimpleTest(TestCase):
self.assertEqual(acme.supplied_part_count, 4)
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.assertEqual(zerg.supplied_part_count, 1)
+ self.assertEqual(zerg.supplied_part_count, 2)
def test_price_breaks(self):
@@ -166,3 +167,53 @@ class ContactSimpleTest(TestCase):
# Remove the parent company
Company.objects.get(pk=self.c.pk).delete()
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)
diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py
index b5ad06019b..b87b0626ae 100644
--- a/InvenTree/company/urls.py
+++ b/InvenTree/company/urls.py
@@ -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'^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'^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'),
@@ -52,9 +53,26 @@ price_break_urls = [
url(r'^(?P\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\d+)/', include(manufacturer_part_detail_urls)),
+]
+
supplier_part_detail_urls = [
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'^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'),
diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py
index bb16ea4bb6..be7d326c36 100644
--- a/InvenTree/company/views.py
+++ b/InvenTree/company/views.py
@@ -24,6 +24,7 @@ from InvenTree.helpers import str2bool
from InvenTree.views import InvenTreeRoleMixin
from .models import Company
+from .models import ManufacturerPart
from .models import SupplierPart
from .models import SupplierPriceBreak
@@ -31,6 +32,7 @@ from part.models import Part
from .forms import EditCompanyForm
from .forms import CompanyImageForm
+from .forms import EditManufacturerPartForm
from .forms import EditSupplierPartForm
from .forms import EditPriceBreakForm
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= -> 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):
""" Detail view for SupplierPart """
model = SupplierPart
@@ -354,11 +527,25 @@ class SupplierPartEdit(AjaxUpdateView):
ajax_template_name = 'modal_form.html'
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):
form = super().get_form()
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
# Therefore the idea to set the value before hiding
if form.is_valid():
@@ -368,6 +555,19 @@ class SupplierPartEdit(AjaxUpdateView):
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):
""" Create view for making new SupplierPart """
@@ -415,6 +615,14 @@ class SupplierPartCreate(AjaxCreateView):
# Save the supplier part object
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)
if single_pricing:
@@ -433,6 +641,12 @@ class SupplierPartCreate(AjaxCreateView):
# Hide the part field
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
def get_initial(self):
@@ -446,6 +660,7 @@ class SupplierPartCreate(AjaxCreateView):
manufacturer_id = self.get_param('manufacturer')
supplier_id = self.get_param('supplier')
part_id = self.get_param('part')
+ manufacturer_part_id = self.get_param('manufacturer_part')
supplier = None
@@ -461,6 +676,16 @@ class SupplierPartCreate(AjaxCreateView):
initials['manufacturer'] = Company.objects.get(pk=manufacturer_id)
except (ValueError, Company.DoesNotExist):
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:
try:
@@ -493,7 +718,7 @@ class SupplierPartDelete(AjaxDeleteView):
"""
success_url = '/supplier/'
- ajax_template_name = 'company/partdelete.html'
+ ajax_template_name = 'company/supplier_part_delete.html'
ajax_form_title = _('Delete Supplier Part')
role_required = 'purchase_order.delete'
diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py
index 96850f4cb0..5c1b104670 100644
--- a/InvenTree/label/models.py
+++ b/InvenTree/label/models.py
@@ -253,10 +253,12 @@ class StockItemLabel(LabelTemplate):
'part': stock_item.part,
'name': stock_item.part.full_name,
'ipn': stock_item.part.IPN,
+ 'revision': stock_item.part.revision,
'quantity': normalize(stock_item.quantity),
'serial': stock_item.serial,
'uid': stock_item.uid,
'qr_data': stock_item.format_barcode(brief=True),
+ 'qr_url': stock_item.format_barcode(url=True, request=request),
'tests': stock_item.testResultMap()
}
diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po
index e5368acac8..ee34b015ea 100644
--- a/InvenTree/locale/de/LC_MESSAGES/django.po
+++ b/InvenTree/locale/de/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \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"
"Last-Translator: Andreas Kaiser , Matthias "
"MAIR\n"
@@ -190,11 +190,15 @@ msgstr "Polnisch"
msgid "Turkish"
msgstr "Türkisch"
-#: InvenTree/status.py:57
+#: InvenTree/status.py:84
msgid "Background worker check failed"
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"
msgstr "InvenTree Status-Überprüfung fehlgeschlagen"
@@ -2045,28 +2049,29 @@ msgid "Supplied Parts"
msgstr "Zulieferer-Teile"
#: company/templates/company/navbar.html:23
-#: order/templates/order/receive_parts.html:14 part/models.py:322
-#: part/templates/part/cat_link.html:7 part/templates/part/category.html:95
+#: order/templates/order/receive_parts.html:14 part/api.py:40
+#: 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:14
#: part/templates/part/category_partlist.html:10
#: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113
#: 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"
msgstr "Teile"
#: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33
#: stock/templates/stock/location.html:100
#: 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"
msgstr "BestandsObjekte"
#: company/templates/company/navbar.html:30
#: company/templates/company/part_navbar.html:14
-#: part/templates/part/navbar.html:36 stock/templates/stock/loc_link.html:7
-#: stock/templates/stock/location.html:29
+#: part/templates/part/navbar.html:36 stock/api.py:51
+#: stock/templates/stock/loc_link.html:7 stock/templates/stock/location.html:29
#: stock/templates/stock/stock_app_base.html:9
#: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180
#: templates/InvenTree/search.html:216
@@ -3274,7 +3279,7 @@ msgstr "Teil-Kategorie"
#: part/models.py:83 part/templates/part/category.html:19
#: 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
msgid "Part Categories"
msgstr "Teil-Kategorien"
@@ -5333,7 +5338,7 @@ msgid "Stock Details"
msgstr "Objekt-Details"
#: 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"
msgstr "Bestand-Lagerorte"
@@ -6137,6 +6142,14 @@ msgstr "Vorlagenteil"
msgid "Assembled part"
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
msgid "Select filter"
msgstr "Filter auswählen"
@@ -6470,6 +6483,22 @@ msgstr "Auftrag zugewiesen"
msgid "No stock items matching query"
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
msgid "Undefined location"
msgstr "unbekannter Lagerort"
@@ -6653,7 +6682,7 @@ msgstr "Elemente, die in Produktion sind, anzeigen"
#: templates/js/table_filters.js:144
msgid "Include Variants"
-msgstr "Varianten hinzufügen"
+msgstr "Varianten einschließen"
#: templates/js/table_filters.js:145
msgid "Include stock items for variant parts"
@@ -6792,7 +6821,7 @@ msgstr "Barcode scannen"
msgid "Admin"
msgstr "Admin"
-#: templates/navbar.html:73 templates/registration/logout.html:5
+#: templates/navbar.html:73
msgid "Logout"
msgstr "Ausloggen"
@@ -6808,6 +6837,18 @@ msgstr "Über InvenBaum"
msgid "QR data not provided"
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
msgid "Enter username"
msgstr "Benutzername eingeben"
@@ -6820,17 +6861,61 @@ msgstr "Passwort"
msgid "Username / password combination is incorrect"
msgstr "Benutzername / Passwort Kombination ist falsch"
-#: templates/registration/logout.html:6
-msgid "You have been logged out"
-msgstr "Sie wurden abgemeldet"
+#: templates/registration/login.html:95
+#: templates/registration/password_reset_form.html:51
+#, fuzzy
+#| msgid "Enter password"
+msgid "Forgotten your password?"
+msgstr "Passwort eingeben"
-#: templates/registration/logout.html:7
-msgid "Click"
-msgstr "Klick"
+#: templates/registration/login.html:95
+msgid "Click here to reset"
+msgstr ""
-#: templates/registration/logout.html:7
-msgid "here to log in
"
-msgstr "hier zum abmelden"
+#: templates/registration/password_reset_complete.html:50
+#, fuzzy
+#| 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
msgid "Server"
@@ -6852,17 +6937,25 @@ msgstr "Gesund"
msgid "Issues detected"
msgstr "Probleme erkannt"
-#: templates/stats.html:30
+#: templates/stats.html:31
msgid "Background Worker"
msgstr "Hintergrund-Prozess"
-#: templates/stats.html:33
-msgid "Operational"
-msgstr "Betriebsbereit"
+#: templates/stats.html:34
+#, fuzzy
+#| msgid "Background Worker"
+msgid "Background worker not running"
+msgstr "Hintergrund-Prozess"
-#: templates/stats.html:35
-msgid "Not running"
-msgstr "Läuft nicht"
+#: templates/stats.html:42
+#, fuzzy
+#| msgid "Part Settings"
+msgid "Email Settings"
+msgstr "Teil-Einstellungen"
+
+#: templates/stats.html:45
+msgid "Email settings not configured"
+msgstr ""
#: templates/stock_table.html:14
msgid "Export Stock Information"
@@ -6980,6 +7073,28 @@ msgstr "Berechtigungen Einträge zu ändern"
msgid "Permission to delete items"
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 to log in"
+#~ msgstr "hier zum abmelden"
+
+#~ msgid "Operational"
+#~ msgstr "Betriebsbereit"
+
+#~ msgid "Not running"
+#~ msgstr "Läuft nicht"
+
#~ msgid "InvenTree server issues detected"
#~ msgstr "InvenTree Server Fehler aufgetreten"
@@ -7009,9 +7124,6 @@ msgstr "Berechtigung Einträge zu löschen"
#~ msgid "customer"
#~ msgstr "Kunde"
-#~ msgid "items"
-#~ msgstr "Teile"
-
#~ msgid "Create purchase order"
#~ msgstr "Neue Bestellung anlegen"
diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po
index fb69f99234..cba1555d33 100644
--- a/InvenTree/locale/en/LC_MESSAGES/django.po
+++ b/InvenTree/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -188,11 +188,15 @@ msgstr ""
msgid "Turkish"
msgstr ""
-#: InvenTree/status.py:57
+#: InvenTree/status.py:84
msgid "Background worker check failed"
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"
msgstr ""
@@ -2022,28 +2026,29 @@ msgid "Supplied Parts"
msgstr ""
#: company/templates/company/navbar.html:23
-#: order/templates/order/receive_parts.html:14 part/models.py:322
-#: part/templates/part/cat_link.html:7 part/templates/part/category.html:95
+#: order/templates/order/receive_parts.html:14 part/api.py:40
+#: 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:14
#: part/templates/part/category_partlist.html:10
#: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113
#: 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"
msgstr ""
#: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33
#: stock/templates/stock/location.html:100
#: 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"
msgstr ""
#: company/templates/company/navbar.html:30
#: company/templates/company/part_navbar.html:14
-#: part/templates/part/navbar.html:36 stock/templates/stock/loc_link.html:7
-#: stock/templates/stock/location.html:29
+#: part/templates/part/navbar.html:36 stock/api.py:51
+#: stock/templates/stock/loc_link.html:7 stock/templates/stock/location.html:29
#: stock/templates/stock/stock_app_base.html:9
#: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180
#: templates/InvenTree/search.html:216
@@ -3242,7 +3247,7 @@ msgstr ""
#: part/models.py:83 part/templates/part/category.html:19
#: 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
msgid "Part Categories"
msgstr ""
@@ -5266,7 +5271,7 @@ msgid "Stock Details"
msgstr ""
#: 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"
msgstr ""
@@ -6063,6 +6068,14 @@ msgstr ""
msgid "Assembled part"
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
msgid "Select filter"
msgstr ""
@@ -6395,6 +6408,18 @@ msgstr ""
msgid "No stock items matching query"
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
msgid "Undefined location"
msgstr ""
@@ -6717,7 +6742,7 @@ msgstr ""
msgid "Admin"
msgstr ""
-#: templates/navbar.html:73 templates/registration/logout.html:5
+#: templates/navbar.html:73
msgid "Logout"
msgstr ""
@@ -6733,6 +6758,16 @@ msgstr ""
msgid "QR data not provided"
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
msgid "Enter username"
msgstr ""
@@ -6745,16 +6780,52 @@ msgstr ""
msgid "Username / password combination is incorrect"
msgstr ""
-#: templates/registration/logout.html:6
-msgid "You have been logged out"
+#: templates/registration/login.html:95
+#: templates/registration/password_reset_form.html:51
+msgid "Forgotten your password?"
msgstr ""
-#: templates/registration/logout.html:7
-msgid "Click"
+#: templates/registration/login.html:95
+msgid "Click here to reset"
msgstr ""
-#: templates/registration/logout.html:7
-msgid "here to log in"
+#: templates/registration/password_reset_complete.html:50
+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 ""
#: templates/stats.html:9
@@ -6777,16 +6848,20 @@ msgstr ""
msgid "Issues detected"
msgstr ""
-#: templates/stats.html:30
+#: templates/stats.html:31
msgid "Background Worker"
msgstr ""
-#: templates/stats.html:33
-msgid "Operational"
+#: templates/stats.html:34
+msgid "Background worker not running"
msgstr ""
-#: templates/stats.html:35
-msgid "Not running"
+#: templates/stats.html:42
+msgid "Email Settings"
+msgstr ""
+
+#: templates/stats.html:45
+msgid "Email settings not configured"
msgstr ""
#: templates/stock_table.html:14
diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po
index fb69f99234..cba1555d33 100644
--- a/InvenTree/locale/es/LC_MESSAGES/django.po
+++ b/InvenTree/locale/es/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -188,11 +188,15 @@ msgstr ""
msgid "Turkish"
msgstr ""
-#: InvenTree/status.py:57
+#: InvenTree/status.py:84
msgid "Background worker check failed"
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"
msgstr ""
@@ -2022,28 +2026,29 @@ msgid "Supplied Parts"
msgstr ""
#: company/templates/company/navbar.html:23
-#: order/templates/order/receive_parts.html:14 part/models.py:322
-#: part/templates/part/cat_link.html:7 part/templates/part/category.html:95
+#: order/templates/order/receive_parts.html:14 part/api.py:40
+#: 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:14
#: part/templates/part/category_partlist.html:10
#: templates/InvenTree/index.html:96 templates/InvenTree/search.html:113
#: 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"
msgstr ""
#: company/templates/company/navbar.html:27 part/templates/part/navbar.html:33
#: stock/templates/stock/location.html:100
#: 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"
msgstr ""
#: company/templates/company/navbar.html:30
#: company/templates/company/part_navbar.html:14
-#: part/templates/part/navbar.html:36 stock/templates/stock/loc_link.html:7
-#: stock/templates/stock/location.html:29
+#: part/templates/part/navbar.html:36 stock/api.py:51
+#: stock/templates/stock/loc_link.html:7 stock/templates/stock/location.html:29
#: stock/templates/stock/stock_app_base.html:9
#: templates/InvenTree/index.html:127 templates/InvenTree/search.html:180
#: templates/InvenTree/search.html:216
@@ -3242,7 +3247,7 @@ msgstr ""
#: part/models.py:83 part/templates/part/category.html:19
#: 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
msgid "Part Categories"
msgstr ""
@@ -5266,7 +5271,7 @@ msgid "Stock Details"
msgstr ""
#: 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"
msgstr ""
@@ -6063,6 +6068,14 @@ msgstr ""
msgid "Assembled part"
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
msgid "Select filter"
msgstr ""
@@ -6395,6 +6408,18 @@ msgstr ""
msgid "No stock items matching query"
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
msgid "Undefined location"
msgstr ""
@@ -6717,7 +6742,7 @@ msgstr ""
msgid "Admin"
msgstr ""
-#: templates/navbar.html:73 templates/registration/logout.html:5
+#: templates/navbar.html:73
msgid "Logout"
msgstr ""
@@ -6733,6 +6758,16 @@ msgstr ""
msgid "QR data not provided"
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
msgid "Enter username"
msgstr ""
@@ -6745,16 +6780,52 @@ msgstr ""
msgid "Username / password combination is incorrect"
msgstr ""
-#: templates/registration/logout.html:6
-msgid "You have been logged out"
+#: templates/registration/login.html:95
+#: templates/registration/password_reset_form.html:51
+msgid "Forgotten your password?"
msgstr ""
-#: templates/registration/logout.html:7
-msgid "Click"
+#: templates/registration/login.html:95
+msgid "Click here to reset"
msgstr ""
-#: templates/registration/logout.html:7
-msgid "here to log in"
+#: templates/registration/password_reset_complete.html:50
+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 ""
#: templates/stats.html:9
@@ -6777,16 +6848,20 @@ msgstr ""
msgid "Issues detected"
msgstr ""
-#: templates/stats.html:30
+#: templates/stats.html:31
msgid "Background Worker"
msgstr ""
-#: templates/stats.html:33
-msgid "Operational"
+#: templates/stats.html:34
+msgid "Background worker not running"
msgstr ""
-#: templates/stats.html:35
-msgid "Not running"
+#: templates/stats.html:42
+msgid "Email Settings"
+msgstr ""
+
+#: templates/stats.html:45
+msgid "Email settings not configured"
msgstr ""
#: templates/stock_table.html:14
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index b22d045258..93b7ffbd8e 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -181,6 +181,13 @@ $("#po-table").inventreeTable({
sortName: 'part__MPN',
field: 'supplier_part_detail.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,
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 54222b7e67..ab946b7dcb 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -8,6 +8,7 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from django.http import JsonResponse
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.response import Response
@@ -36,7 +37,7 @@ from InvenTree.status_codes import BuildStatus
class PartCategoryTree(TreeSerializer):
- title = "Parts"
+ title = _("Parts")
model = PartCategory
queryset = PartCategory.objects.all()
diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py
index f3cf598225..b0f53c55eb 100644
--- a/InvenTree/part/bom.py
+++ b/InvenTree/part/bom.py
@@ -16,7 +16,7 @@ from InvenTree.helpers import DownloadFile, GetExportFormats
from .admin import BomItemResource
from .models import BomItem
-from company.models import SupplierPart
+from company.models import ManufacturerPart, SupplierPart
def IsValidBOMFormat(fmt):
@@ -49,7 +49,7 @@ def MakeBomTemplate(fmt):
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.
Args:
@@ -160,7 +160,123 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
# Add stock columns to dataset
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
"""
@@ -169,8 +285,6 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
manufacturer_headers = [
_('Supplier'),
_('SKU'),
- _('Manufacturer'),
- _('MPN'),
]
manufacturer_cols = {}
@@ -191,31 +305,18 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
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
# Generate column names for this supplier
k_sup = manufacturer_headers[0] + "_" + str(idx)
k_sku = manufacturer_headers[1] + "_" + str(idx)
- k_man = manufacturer_headers[2] + "_" + str(idx)
- k_mpn = manufacturer_headers[3] + "_" + str(idx)
try:
manufacturer_cols[k_sup].update({b_idx: supplier_name})
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:
manufacturer_cols[k_sup] = {b_idx: supplier_name}
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_columns_to_dataset(manufacturer_cols, len(bom_items))
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index 63cd7b4399..131c6aeac7 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -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"))
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"))
-
+
def get_choices(self):
""" BOM export format choices """
diff --git a/InvenTree/part/templates/part/manufacturer.html b/InvenTree/part/templates/part/manufacturer.html
new file mode 100644
index 0000000000..7643d4d996
--- /dev/null
+++ b/InvenTree/part/templates/part/manufacturer.html
@@ -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 %}
+
+
+
+
+
+{% 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 %}
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html
index 17e69172de..d8750a49a5 100644
--- a/InvenTree/part/templates/part/navbar.html
+++ b/InvenTree/part/templates/part/navbar.html
@@ -69,6 +69,12 @@
{% endif %}
{% if part.purchaseable and roles.purchase_order.view %}
+
+
+
+ {% trans "Manufacturers" %}
+
+
diff --git a/InvenTree/part/templates/part/partial_delete.html b/InvenTree/part/templates/part/partial_delete.html
index 7b51bf2e89..11910eec66 100644
--- a/InvenTree/part/templates/part/partial_delete.html
+++ b/InvenTree/part/templates/part/partial_delete.html
@@ -1,14 +1,15 @@
{% extends "modal_form.html" %}
+{% load i18n %}
{% block pre_form_content %}
- Are you sure you want to delete part '{{ part.full_name }} '?
+ {% trans "Are you sure you want to delete part" %} '{{ part.full_name }} '?
{% if part.used_in_count %}
-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:
+
{% 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" %}:
{% for child in part.used_in.all %}
{{ child.part.full_name }} - {{ child.part.description }}
@@ -18,7 +19,7 @@
{% if part.stock_items.all|length > 0 %}
-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:
+
{% 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" %}:
{% for stock in part.stock_items.all %}
{{ stock }}
@@ -27,9 +28,20 @@
{% endif %}
+{% if part.manufacturer_parts.all|length > 0 %}
+
+{% 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" %}:
+
+ {% for spart in part.manufacturer_parts.all %}
+ {{ spart.manufacturer.name }} - {{ spart.MPN }}
+ {% endfor %}
+
+
+{% endif %}
+
{% if part.supplier_parts.all|length > 0 %}
-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.
+
{% 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" %}:
{% for spart in part.supplier_parts.all %}
{{ spart.supplier.name }} - {{ spart.SKU }}
@@ -40,7 +52,7 @@
{% if part.serials.all|length > 0 %}
-There are {{ part.serials.all|length }} unique parts tracked for '{{ part.full_name }}'. Deleting this part will permanently remove this tracking information.
+{% 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" %}.
{% endif %}
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html
index 1093129412..313d48f0ad 100644
--- a/InvenTree/part/templates/part/supplier.html
+++ b/InvenTree/part/templates/part/supplier.html
@@ -85,7 +85,7 @@
{
params: {
part: {{ part.id }},
- part_detail: true,
+ part_detail: false,
supplier_detail: true,
manufacturer_detail: true,
},
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index f275edede2..f1185fbe8c 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -60,6 +60,7 @@ part_detail_urls = [
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'^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'^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'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 6d323acf33..8787a14b35 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -1845,6 +1845,8 @@ class BomDownload(AjaxView):
supplier_data = str2bool(request.GET.get('supplier_data', False))
+ manufacturer_data = str2bool(request.GET.get('manufacturer_data', False))
+
levels = request.GET.get('levels', None)
if levels is not None:
@@ -1866,7 +1868,9 @@ class BomDownload(AjaxView):
max_levels=levels,
parameter_data=parameter_data,
stock_data=stock_data,
- supplier_data=supplier_data)
+ supplier_data=supplier_data,
+ manufacturer_data=manufacturer_data,
+ )
def get_data(self):
return {
@@ -1896,6 +1900,7 @@ class BomExport(AjaxView):
parameter_data = str2bool(request.POST.get('parameter_data', False))
stock_data = str2bool(request.POST.get('stock_data', False))
supplier_data = str2bool(request.POST.get('supplier_data', False))
+ manufacturer_data = str2bool(request.POST.get('manufacturer_data', False))
try:
part = Part.objects.get(pk=self.kwargs['pk'])
@@ -1913,6 +1918,7 @@ class BomExport(AjaxView):
url += '¶meter_data=' + str(parameter_data)
url += '&stock_data=' + str(stock_data)
url += '&supplier_data=' + str(supplier_data)
+ url += '&manufacturer_data=' + str(manufacturer_data)
if levels:
url += '&levels=' + str(levels)
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 7bad9df83e..6da3962224 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -48,7 +48,7 @@ from rest_framework import generics, filters, permissions
class StockCategoryTree(TreeSerializer):
- title = 'Stock'
+ title = _('Stock')
model = StockLocation
@property
@@ -774,7 +774,7 @@ class StockList(generics.ListCreateAPIView):
company = params.get('company', 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
supplier = params.get('supplier', None)
@@ -786,7 +786,7 @@ class StockList(generics.ListCreateAPIView):
manufacturer = params.get('manufacturer', 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):
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index b57f96d0f7..ed86a807e1 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -344,6 +344,8 @@ class StockItem(MPTTModel):
"stockitem",
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}),
},
**kwargs
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index 9cb2538ba7..5b00c1dd17 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -84,7 +84,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
'sales_order',
'supplier_part',
'supplier_part__supplier',
- 'supplier_part__manufacturer',
+ 'supplier_part__manufacturer_part__manufacturer',
'allocations',
'sales_order_allocations',
'location',
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index 04518a0c65..b955acda9c 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -331,9 +331,21 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{{ item.link }}
{% endif %}
- {% if item.supplier_part %}
+ {% if item.supplier_part.manufacturer_part %}
+ {% trans "Manufacturer" %}
+ {{ item.supplier_part.manufacturer_part.manufacturer.name }}
+
+
+
+ {% trans "Manufacturer Part" %}
+ {{ item.supplier_part.manufacturer_part.MPN }}
+
+ {% endif %}
+ {% if item.supplier_part %}
+
+
{% trans "Supplier" %}
{{ item.supplier_part.supplier.name }}
diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html
index b65ec00c00..d919872b3e 100644
--- a/InvenTree/templates/InvenTree/search.html
+++ b/InvenTree/templates/InvenTree/search.html
@@ -2,6 +2,7 @@
{% load static %}
{% load i18n %}
+{% load inventree_extras %}
{% block page_title %}
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');
loadSupplierPartTable(
@@ -287,6 +303,15 @@ InvenTree | {% trans "Search Results" %}
{% if roles.purchase_order.view or roles.sales_order.view %}
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 %}
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 %}
{% if roles.sales_order.view %}
diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js
index 601d4a5370..d258c8bab1 100644
--- a/InvenTree/templates/js/company.js
+++ b/InvenTree/templates/js/company.js
@@ -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 += ` `;
+ }
+
+ if (row.part_detail.assembly) {
+ html += ` `;
+ }
+
+ if (!row.part_detail.active) {
+ html += `{% trans "Inactive" %} `;
+ }
+
+ 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) {
/*
* Load supplier part table
@@ -133,10 +231,11 @@ function loadSupplierPartTable(table, url, options) {
switchable: false,
},
{
+ visible: params['part_detail'],
+ switchable: params['part_detail'],
sortable: true,
field: 'part_detail.full_name',
title: '{% trans "Part" %}',
- switchable: false,
formatter: function(value, row, index, field) {
var url = `/part/${row.part}/`;
@@ -183,6 +282,8 @@ function loadSupplierPartTable(table, url, options) {
}
},
{
+ visible: params['manufacturer_detail'],
+ switchable: params['manufacturer_detail'],
sortable: true,
field: 'manufacturer',
title: '{% trans "Manufacturer" %}',
@@ -199,9 +300,18 @@ function loadSupplierPartTable(table, url, options) {
}
},
{
+ visible: params['manufacturer_detail'],
+ switchable: params['manufacturer_detail'],
sortable: true,
field: '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',
diff --git a/InvenTree/templates/js/filters.js b/InvenTree/templates/js/filters.js
index 01b74763e0..612af8e03c 100644
--- a/InvenTree/templates/js/filters.js
+++ b/InvenTree/templates/js/filters.js
@@ -164,11 +164,11 @@ function getFilterOptionList(tableKey, filterKey) {
return {
'1': {
key: '1',
- value: 'true',
+ value: '{% trans "true" %}',
},
'0': {
key: '0',
- value: 'false',
+ value: '{% trans "false" %}',
},
};
} else if ('options' in settings) {
@@ -394,8 +394,8 @@ function getFilterOptionValue(tableKey, filterKey, valueKey) {
// Lookup for boolean options
if (filter.type == 'bool') {
- if (value == '1') return 'true';
- if (value == '0') return 'false';
+ if (value == '1') return '{% trans "true" %}';
+ if (value == '0') return '{% trans "false" %}';
return value;
}
diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js
index b163bc89f3..33f2dae8d6 100644
--- a/InvenTree/templates/js/stock.js
+++ b/InvenTree/templates/js/stock.js
@@ -354,7 +354,7 @@ function loadStockTable(table, options) {
var html = imageHoverIcon(row.part_detail.thumbnail);
html += row.part_detail.full_name;
- html += ` (${data.length} items) `;
+ html += ` (${data.length} {% trans "items" %}) `;
html += makePartIcons(row.part_detail);
@@ -446,7 +446,7 @@ function loadStockTable(table, options) {
});
if (batches.length > 1) {
- return "" + batches.length + " batches";
+ return "" + batches.length + " {% trans 'batches' %}";
} else if (batches.length == 1) {
if (batches[0]) {
return batches[0];
@@ -473,9 +473,9 @@ function loadStockTable(table, options) {
// Single location, easy!
return locations[0];
} else if (locations.length > 1) {
- return "In " + locations.length + " locations";
+ return "In " + locations.length + " {% trans 'locations' %}";
} else {
- return "{% trans "Undefined location" %} ";
+ return "{% trans 'Undefined location' %} ";
}
} else if (field == 'notes') {
var notes = [];
@@ -1219,7 +1219,7 @@ function loadInstalledInTable(table, options) {
// Add some buttons yo!
html += ``;
- html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans "Uninstall stock item" %}");
+ html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans 'Uninstall stock item' %}");
html += `
`;
diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html
index acd71f0cd8..55e0e06018 100644
--- a/InvenTree/templates/navbar.html
+++ b/InvenTree/templates/navbar.html
@@ -59,11 +59,17 @@
{% endif %}
+ {% if user.is_staff %}
{% if not system_healthy %}
-
+ {% if not django_q_running %}
+
+ {% else %}
+
+ {% endif %}
{% elif not up_to_date %}
{% endif %}
+ {% endif %}
{{ user.get_username }}